1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
//! All responses that can be received from the API.
pub mod canvases;
pub mod config;
pub mod filters;
pub mod general;
pub(crate) mod hotkeys;
pub(crate) mod ids;
pub mod inputs;
pub mod media_inputs;
pub mod outputs;
pub mod profiles;
pub mod recording;
pub(crate) mod replay_buffer;
pub mod scene_collections;
pub mod scene_items;
pub mod scenes;
pub mod sources;
pub mod streaming;
pub mod transitions;
pub mod ui;
pub(crate) mod virtual_cam;
use derive_more::derive::TryFrom;
use serde::{Deserialize, Deserializer, Serialize, de};
#[derive(Debug)]
pub(crate) enum ServerMessage {
/// First message sent from the server immediately on client connection. Contains
/// authentication information if authentication is required. Also contains RPC version for
/// version negotiation.
Hello(Hello),
/// The identify request was received and validated, and the connection is now ready for normal
/// operation.
Identified(Identified),
/// An event coming from OBS has occurred. For example scene switched, source muted.
#[cfg(feature = "events")]
Event(crate::events::Event),
#[cfg(not(feature = "events"))]
Event,
/// `obs-websocket` is responding to a request coming from a client.
RequestResponse(RequestResponse),
/// `obs-websocket` is responding to a request batch coming from the client.
#[allow(dead_code)]
RequestBatchResponse(RequestBatchResponse),
}
impl<'de> Deserialize<'de> for ServerMessage {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct RawServerMessage {
#[serde(rename = "op")]
op_code: OpCode,
#[serde(rename = "d")]
data: serde_json::Value,
}
#[derive(Deserialize, TryFrom)]
#[serde(try_from = "u8")]
#[try_from(repr)]
#[repr(u8)]
enum OpCode {
/// The initial message sent by obs-websocket to newly connected clients.
Hello = 0,
/// The response sent by obs-websocket to a client after it has successfully identified
/// with obs-websocket.
Identified = 2,
/// The message sent by obs-websocket containing an event payload.
Event = 5,
/// The message sent by obs-websocket in response to a particular request from a
/// client.
RequestResponse = 7,
/// The message sent by obs-websocket in response to a particular batch of requests
/// from a client.
RequestBatchResponse = 9,
}
let raw = RawServerMessage::deserialize(deserializer)?;
Ok(match raw.op_code {
OpCode::Hello => {
ServerMessage::Hello(serde_json::from_value(raw.data).map_err(de::Error::custom)?)
}
OpCode::Identified => ServerMessage::Identified(
serde_json::from_value(raw.data).map_err(de::Error::custom)?,
),
OpCode::Event => {
#[cfg(feature = "events")]
{
ServerMessage::Event(
serde_json::from_value(raw.data).map_err(de::Error::custom)?,
)
}
#[cfg(not(feature = "events"))]
{
ServerMessage::Event
}
}
OpCode::RequestResponse => ServerMessage::RequestResponse(
serde_json::from_value(raw.data).map_err(de::Error::custom)?,
),
OpCode::RequestBatchResponse => ServerMessage::RequestBatchResponse(
serde_json::from_value(raw.data).map_err(de::Error::custom)?,
),
})
}
}
/// First message sent from the server immediately on client connection. Contains authentication
/// information if authentication is required. Also contains RPC version for version negotiation.
#[derive(Debug, Deserialize)]
pub(crate) struct Hello {
#[allow(dead_code)]
#[serde(rename = "obsWebSocketVersion")]
pub obs_web_socket_version: semver::Version,
/// version number which gets incremented on each **breaking change** to the obs-websocket
/// protocol. Its usage in this context is to provide the current RPC version that the server
/// would like to use.
#[serde(rename = "rpcVersion")]
pub rpc_version: u32,
#[serde(rename = "authentication")]
pub authentication: Option<Authentication>,
}
/// The identify request was received and validated, and the connection is now ready for normal
/// operation.
#[derive(Debug, Deserialize)]
pub(crate) struct Identified {
/// The RPC (remote procedure call) version to be used.
#[serde(rename = "negotiatedRpcVersion")]
pub negotiated_rpc_version: u32,
}
/// `obs-websocket` is responding to a request coming from a client.
#[derive(Debug, Deserialize)]
pub(crate) struct RequestResponse {
#[allow(dead_code)]
#[serde(rename = "requestType")]
pub r#type: String,
#[serde(rename = "requestId")]
pub id: String,
#[serde(rename = "requestStatus")]
pub status: Status,
#[serde(rename = "responseData", default)]
pub data: serde_json::Value,
}
#[derive(Debug, Deserialize)]
pub(crate) struct RequestBatchResponse {
#[allow(dead_code)]
#[serde(rename = "requestId")]
pub id: String,
#[allow(dead_code)]
pub results: Vec<serde_json::Value>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct Authentication {
pub challenge: String,
pub salt: String,
}
#[derive(Debug, Deserialize)]
pub(crate) struct Status {
/// Is true if the request resulted in [`StatusCode::Success`]. False if otherwise.
pub result: bool,
pub code: StatusCode,
/// May be provided by the server on errors to offer further details on why a request failed.
pub comment: Option<String>,
}
/// The status code gives information about the result of a request. It gives further insight into
/// what went wrong, if a request failed.
#[derive(
Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize, TryFrom,
)]
#[serde(into = "u16", try_from = "u16")]
#[try_from(repr)]
#[repr(u16)]
#[non_exhaustive]
pub enum StatusCode {
/// Unknown status, should never be used.
Unknown = 0,
/// For internal use to signify a successful field check.
NoError = 10,
/// The request has succeeded.
Success = 100,
/// The `requestType` field is missing from the request data.
MissingRequestType = 203,
/// The request type is invalid or does not exist.
UnknownRequestType = 204,
/// Generic error code.
///
/// **Note:** A comment is required to be provided by obs-websocket.
GenericError = 205,
/// The request batch execution type is not supported.
UnsupportedRequestBatchExecutionType = 206,
/// The server is not ready to handle the request.
///
/// **Note:** This usually occurs during OBS scene collection change or exit. Requests may be
/// tried again after a delay if this code is given.
NotReady = 207,
/// A required request field is missing.
MissingRequestField = 300,
/// The request does not have a valid `requestData` object.
MissingRequestData = 301,
/// Generic invalid request field message.
///
/// **Note:** A comment is required to be provided by obs-websocket.
InvalidRequestField = 400,
/// A request field has the wrong data type.
InvalidRequestFieldType = 401,
/// A request field (number) is outside the allowed range.
RequestFieldOutOfRange = 402,
/// A request field (string or array) is empty and cannot be.
RequestFieldEmpty = 403,
/// There are too many request fields (For example a request takes two optional fields, where
/// only one is allowed at a time).
TooManyRequestFields = 404,
/// An output is running and cannot be in order to perform the request.
OutputRunning = 500,
/// An output is not running and should be.
OutputNotRunning = 501,
/// An output is paused and should not be.
OutputPaused = 502,
/// An output is not paused and should be.
OutputNotPaused = 503,
/// An output is disabled and should not be.
OutputDisabled = 504,
/// Studio mode is active and cannot be.
StudioModeActive = 505,
/// Studio mode is not active and should be.
StudioModeNotActive = 506,
/// The resource was not found.
///
/// **Note:** Resources are any kind of object in obs-websocket, like inputs, profiles,
/// outputs, etc.
ResourceNotFound = 600,
/// The resource already exists.
ResourceAlreadyExists = 601,
/// The type of resource found is invalid.
InvalidResourceType = 602,
/// There are not enough instances of the resource in order to perform the request.
NotEnoughResources = 603,
/// The state of the resource is invalid. For example, if the resource is blocked from being
/// accessed.
InvalidResourceState = 604,
/// The specified input (obs_source_t-OBS_SOURCE_TYPE_INPUT) had the wrong kind.
InvalidInputKind = 605,
/// The resource does not support being configured.
///
/// This is particularly relevant to transitions, where they do not always have changeable
/// settings.
ResourceNotConfigurable = 606,
/// The specified filter had the wrong kind.
InvalidFilterKind = 607,
/// Creating the resource failed.
ResourceCreationFailed = 700,
/// Performing an action on the resource failed.
ResourceActionFailed = 701,
/// Processing the request failed unexpectedly.
///
/// **Note:** A comment is required to be provided by obs-websocket.
RequestProcessingFailed = 702,
/// The combination of request fields cannot be used to perform an action.
CannotAct = 703,
}
impl From<StatusCode> for u16 {
fn from(value: StatusCode) -> Self {
value as Self
}
}
/// Additional close codes, defined by `obs-websocket`. These can be used to further pin down the
/// details of why the web-socket connection was closed.
#[derive(
Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize, TryFrom,
)]
#[serde(into = "u16", try_from = "u16")]
#[try_from(repr)]
#[repr(u16)]
#[non_exhaustive]
pub enum WebSocketCloseCode {
/// Unknown reason, should never be used.
UnknownReason = 4000,
/// The server was unable to decode the incoming web-socket message.
MessageDecodeError = 4002,
/// A data field is required but missing from the payload.
MissingDataField = 4003,
/// A data field's value type is invalid.
InvalidDataFieldType = 4004,
/// A data field's value is invalid.
InvalidDataFieldValue = 4005,
/// The specified `op` was invalid or missing.
UnknownOpCode = 4006,
/// The client sent a web-socket message without first sending `Identify` message.
NotIdentified = 4007,
/// The client sent an `Identify` message while already identified.
///
/// **Note:** Once a client has identified, only `Reidentify` may be used to change session
/// parameters.
AlreadyIdentified = 4008,
/// The authentication attempt (via `Identify`) failed.
AuthenticationFailed = 4009,
/// The server detected the usage of an old version of the obs-websocket RPC protocol.
UnsupportedRpcVersion = 4010,
/// The web-socket session has been invalidated by the obs-websocket server.
///
/// **Note:** This is the code used by the `Kick` button in the UI Session List. If you receive
/// this code, you must not automatically reconnect.
SessionInvalidated = 4011,
/// A requested feature is not supported due to hardware/software limitations.
UnsupportedFeature = 4012,
}
impl From<WebSocketCloseCode> for u16 {
fn from(value: WebSocketCloseCode) -> Self {
value as Self
}
}