docs: Overhaul documentation (#863)

More docs-related commits will follow, but this needs to be merged in order to continue with other development.

* Docs: Overhaul docs generator (beginning)

* docs: Rename comments file

* docs: Move comments gitignore

* docs: Initial request documentation

* docs: Improvements to comment processing

* docs: More improvements

* docs: Add enum functionality for protocol.json

* WebSocketServer: Document enums

* RequestHandler: Document RequestStatus enum

* Base: Move ObsWebSocketRequestBatchExecutionType to its own file

Moves it to its own file, renaming it to `RequestBatchExecutionType`.
Changes the RPC to use integer values for selecting execution type
instead of strings.

* docs: Update introduction header

Removes the enum section, and documents RequestBatchExecutionType.

* WebSocketCloseCode: Shuffle a bit

* Base: Use `field` instead of `key` or `parameter` in most places

* RequestStatus: Mild shuffle

It was really bothering me that OutputPaused and OutputNotPaused
had to be separated, so we're breaking it while we're breaking
other stuff.

* docs: Delete old files

They may be added back in some form, but for now I'm getting them
out of the way.

* docs: Add enum identifier value

Forgot to add this before, oops

* docs: Document more enums

* docs: Add basic protocol.md generator

* docs: More work on MD generator

* docs: MD generator should be finished now

* docs: More fixes

* docs: More fixes

* docs: More tweaks + add readme

* docs: Update readme and add inputs docs

* docs: More documentation
This commit is contained in:
tt2468 2021-12-10 21:38:18 -08:00 committed by GitHub
parent 6cec018c8d
commit fcbe11616d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 7109 additions and 875 deletions

View File

@ -3,13 +3,13 @@ set -e
echo "-- Generating documentation."
echo "-- Node version: $(node -v)"
echo "-- NPM version: $(npm -v)"
echo "-- Python3 version: $(python3 -V)"
git fetch origin
git checkout ${CHECKOUT_REF/refs\/heads\//}
cd docs
npm install
npm run build
bash build_docs.sh
echo "-- Documentation successfully generated."

View File

@ -135,6 +135,7 @@ set(obs-websocket_HEADERS
src/eventhandler/types/EventSubscription.h
src/requesthandler/RequestHandler.h
src/requesthandler/types/RequestStatus.h
src/requesthandler/types/RequestBatchExecutionType.h
src/requesthandler/rpc/Request.h
src/requesthandler/rpc/RequestResult.h
src/forms/SettingsDialog.h

View File

@ -1,11 +0,0 @@
[*]
end_of_line = lf
charset = utf-8
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
insert_final_newline = true
[*.md, *.mustache]
trim_trailing_whitespace = false
insert_final_newline = false

5
docs/.gitignore vendored
View File

@ -1,4 +1 @@
node_modules
logs
*.log
npm-debug.log*
work

View File

@ -1,21 +1,127 @@
## Installation
# obs-websocket documentation
Install node and update npm if necessary.
This is the documentation for obs-websocket. Run build_docs.sh to auto generate the latest docs from the `src` directory. There are 3 components to the docs generation:
- `comments/comments.js`: Generates the `work/comments.json` file from the code comments in the src directory.
- `docs/process_comments.py`: Processes `work/comments.json` to create `generated/protocol.json`, which is a machine-readable documentation format that can be used to create obs-websocket client libraries.
- `docs/generate_md.py`: Processes `generated/protocol.json` to create `generated/protocol.md`, which is the actual human-readable documentation.
```sh
cd obs-websocket/docs
npm install
Some notes about documenting:
- The `complexity` comment line is a suggestion to the user about how much knowledge about OBS's inner workings is required to safely use the associated feature. `1` for easy, `5` for megadeath-expert.
- The `rpcVersion` comment line is used to specify the latest available version that the feature is available in. If a feature is deprecated, then the placeholder value of `-1` should be replaced with the last RPC version that the feature will be available in. Manually specifying an RPC version automatically adds the `Deprecated` line to the entry in `generated/protocol.md`.
- The description can be multiple lines, but must be contained above the first documentation property (the lines starting with `@`).
- Value types are in reference to JSON values. The only ones you should use are `Any`, `String`, `Boolean`, `Number`, `Array`, `Object`.
- `Array` types follow this format: `Array<subtype>`, for example `Array<String>` to specify an array of strings.
Formatting notes:
- Fields should have their columns aligned. So in a request, the columns of all `@requestField`s should be aligned.
- We suggest looking at how other enums/events/requests have been documented before documenting one of your own, to get a general feel of how things have been formatted.
## Creating enum documentation
Enums follow this code comment format:
```
/**
* [description]
*
* @enumIdentifier [identifier]
* @enumValue [value]
* @enumType [type]
* @rpcVersion [latest available RPC version, use `-1` unless deprecated.]
* @initialVersion [first obs-websocket version this is found in]
* @api enums
*/
```
## Build
```sh
# Just extract the comments.
npm run comments
# Just render the markdown.
npm run docs
# Do both comments and markdown.
npm run build
Example code comment:
```
/**
* The initial message sent by obs-websocket to newly connected clients.
*
* @enumIdentifier Hello
* @enumValue 0
* @enumType WebSocketOpCode
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
```
- This is the documentation for the `WebSocketOpCode::Hello` enum identifier.
## Creating event documentation
Events follow this code comment format:
```
/**
* [description]
*
* @dataField [field name] | [value type] | [field description]
* [... more @dataField entries ...]
*
* @eventType [type]
* @eventSubscription [EventSubscription requirement]
* @complexity [complexity rating, 1-5]
* @rpcVersion [latest available RPC version, use `-1` unless deprecated.]
* @initialVersion [first obs-websocket version this is found in]
* @category [event category]
* @api events
*/
```
Example code comment:
```
/**
* Studio mode has been enabled or disabled.
*
* @dataField studioModeEnabled | Boolean | True == Enabled, False == Disabled
*
* @eventType StudioModeStateChanged
* @eventSubscription General
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category general
* @api events
*/
```
## Creating request documentation
Requests follow this code comment format:
```
/**
* [description]
*
* @requestField [optional flag][field name] | [value type] | [field description] | [value restrictions (only include if the value type is `Number`)] | [default behavior (only include if optional flag is set)]
* [... more @requestField entries ...]
*
* @responseField [field name] | [value type] | [field description]
* [... more @responseField entries ...]
*
* @requestType [type]
* @complexity [complexity rating, 1-5]
* @rpcVersion [latest available RPC version, use `-1` unless deprecated.]
* @initialVersion [first obs-websocket version this is found in]
* @category [request category]
* @api requests
*/
```
- The optional flag is a `?` that prefixes the field name, telling the docs processor that the field is optionally specified.
Example code comment:
```
/**
* Gets the value of a "slot" from the selected persistent data realm.
*
* @requestField realm | String | The data realm to select. `OBS_WEBSOCKET_DATA_REALM_GLOBAL` or `OBS_WEBSOCKET_DATA_REALM_PROFILE`
* @requestField slotName | String | The name of the slot to retrieve data from
*
* @responseField slotValue | String | Value associated with the slot. `null` if not set
*
* @requestType GetPersistentData
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api requests
*/
```

9
docs/build_docs.sh Executable file
View File

@ -0,0 +1,9 @@
#!/bin/bash
cd comments
npm install
npm run comments
cd ../docs
python3 process_comments.py
python3 generate_md.py

View File

@ -1,104 +0,0 @@
const fs = require('fs');
const path = require('path');
const glob = require('glob');
const parseComments = require('parse-comments');
const config = require('./config.json');
/**
* Read each file and call `parse-comments` on it.
*
* @param {String|Array} `files` List of file paths to read from.
* @return {Object|Array} Array of `parse-comments` objects.
*/
const parseFiles = files => {
let response = [];
files.forEach(file => {
const f = fs.readFileSync(file, 'utf8').toString();
response = response.concat(parseComments(f));
});
return response;
};
/**
* Filters/sorts the results from `parse-comments`.
* @param {Object|Array} `comments` Array of `parse-comments` objects.
* @return {Object} Filtered comments sorted by `@api` and `@category`.
*/
const processComments = comments => {
let sorted = {};
let errors = [];
comments.forEach(comment => {
if (comment.typedef) {
comment.comment = undefined;
comment.context = undefined;
sorted['typedefs'] = sorted['typedefs'] || [];
sorted['typedefs'].push(comment);
return;
}
if (typeof comment.api === 'undefined') return;
let validationFailures = validateComment(comment);
if (validationFailures) {
errors.push(validationFailures);
}
// Store the object based on its api (ie. requests, events) and category (ie. general, scenes, etc).
comment.category = comment.category || 'miscellaneous';
// Remove some unnecessary properties to avoid result differences in travis.
comment.comment = undefined;
comment.context = undefined;
// Create an entry in sorted for the api/category if one does not exist.
sorted[comment.api] = sorted[comment.api] || {};
sorted[comment.api][comment.category] = sorted[comment.api][comment.category] || [];
// Store the comment in the appropriate api/category.
sorted[comment.api][comment.category].push(comment);
});
if (errors.length) {
throw JSON.stringify(errors, null, 2);
}
return sorted;
};
// Rudimentary validation of documentation content, returns an error object or undefined.
const validateComment = comment => {
let errors = [];
[].concat(comment.params).concat(comment.returns).filter(Boolean).forEach(param => {
if (typeof param.name !== 'string' || param.name === '') {
errors.push({
description: `Invalid param or return value name`,
param: param
});
}
if (typeof param.type !== 'string' || param.type === '') {
errors.push({
description: `Invalid param or return value type`,
param: param
});
}
});
if (errors.length) {
return {
errors: errors,
fullContext: Object.assign({}, comment)
};
}
};
const files = glob.sync(config.srcGlob);
const comments = processComments(parseFiles(files));
if (!fs.existsSync(config.outDirectory)){
fs.mkdirSync(config.outDirectory);
}
fs.writeFileSync(path.join(config.outDirectory, 'comments.json'), JSON.stringify(comments, null, 2));

2
docs/comments/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
npm-debug.log*

24
docs/comments/comments.js Normal file
View File

@ -0,0 +1,24 @@
const fs = require('fs');
const path = require('path');
const glob = require('glob');
const parseComments = require('parse-comments');
const config = require('./config.json');
const parseFiles = files => {
let response = [];
files.forEach(file => {
const f = fs.readFileSync(file, 'utf8').toString();
response = response.concat(parseComments(f));
});
return response;
};
const files = glob.sync(config.srcGlob);
const comments = parseFiles(files);
if (!fs.existsSync(config.outDirectory)){
fs.mkdirSync(config.outDirectory);
}
fs.writeFileSync(path.join(config.outDirectory, 'comments.json'), JSON.stringify(comments, null, 2));

View File

@ -0,0 +1,4 @@
{
"srcGlob": "./../../src/**/*.@(cpp|h)",
"outDirectory": "./../work"
}

View File

@ -0,0 +1,15 @@
{
"name": "obs-websocket-comments",
"version": "1.2.0",
"description": "",
"main": "comments.js",
"scripts": {
"comments": "node ./comments.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"glob": "^7.1.2",
"parse-comments": "^0.4.3"
}
}

View File

@ -1,5 +0,0 @@
{
"srcGlob": "./../src/**/*.@(cpp|h)",
"srcTemplate": "./protocol.hbs",
"outDirectory": "./generated"
}

View File

@ -1,37 +0,0 @@
const fs = require('fs');
const path = require('path');
const toc = require('markdown-toc');
const handlebars = require('handlebars');
const config = require('./config.json');
const helpers = require('handlebars-helpers')({
handlebars: handlebars
});
// Allows pipe characters to be used within markdown tables.
handlebars.registerHelper('depipe', (text) => {
return typeof text === 'string' ? text.replace('|', '\\|') : text;
});
const insertHeader = (text) => {
return '<!-- This file was generated based on handlebars templates. Do not edit directly! -->\n\n' + text;
};
/**
* Writes `protocol.md` using `protocol.mustache`.
*
* @param {Object} `data` Data to assign to the mustache template.
*/
const generateProtocol = (templatePath, data) => {
const template = fs.readFileSync(templatePath).toString();
const generated = handlebars.compile(template)(data);
return insertHeader(toc.insert(generated));
};
if (!fs.existsSync(config.outDirectory)){
fs.mkdirSync(config.outDirectory);
}
const comments = fs.readFileSync(path.join(config.outDirectory, 'comments.json'), 'utf8');
const markdown = generateProtocol(config.srcTemplate, JSON.parse(comments));
fs.writeFileSync(path.join(config.outDirectory, 'protocol.md'), markdown);

302
docs/docs/generate_md.py Normal file
View File

@ -0,0 +1,302 @@
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s [generate_md.py] [%(levelname)s] %(message)s")
import os
import sys
import json
enumTypeOrder = [
'WebSocketOpCode',
'WebSocketCloseCode',
'RequestBatchExecutionType',
'RequestStatus',
'EventSubscription'
]
categoryOrder = [
'General',
'Config',
'Sources',
'Scenes',
'Inputs',
'Transitions',
'Filters',
'Scene Items',
'Outputs',
'Stream',
'Record',
'Media Inputs',
'High-Volume'
]
requestFieldHeader = """
**Request Fields:**
| Name | Type | Description | Value Restrictions | ?Default Behavior |
| ---- | :---: | ----------- | :----------------: | ----------------- |
"""
responseFieldHeader = """
**Response Fields:**
| Name | Type | Description |
| ---- | :---: | ----------- |
"""
dataFieldHeader = """
**Data Fields:**
| Name | Type | Description |
| ---- | :---: | ----------- |
"""
fragments = []
# Utils
#######################################################################################################################
def read_file(fileName):
with open(fileName, 'r') as f:
return f.read()
def get_fragment(name, register = True):
global fragments
testFragmentName = name.replace(' ', '-').replace(':', '').lower()
if testFragmentName in fragments:
testFragmentName += '-1'
increment = 1
while testFragmentName in fragments:
increment += 1
testFragmentName[:-1] = str(increment)
if register:
fragments.append(testFragmentName)
return testFragmentName
def get_category_items(items, category):
ret = []
for item in requests:
if item['category'] != category:
continue
ret.append(item)
return ret
def get_enums_toc(enums):
ret = ''
for enumType in enumTypeOrder:
enum = None
for enumIt in enums:
if enumIt['enumType'] == enumType:
enum = enumIt
break
if not enum:
continue
typeFragment = get_fragment(enumType, False)
ret += '- [{}](#{})\n'.format(enumType, typeFragment)
for enumIdentifier in enum['enumIdentifiers']:
enumIdentifier = enumIdentifier['enumIdentifier']
enumIdentifierHeader = '{}::{}'.format(enumType, enumIdentifier)
enumIdentifierFragment = get_fragment(enumIdentifierHeader, False)
ret += ' - [{}](#{})\n'.format(enumIdentifierHeader, enumIdentifierFragment)
return ret
def get_enums(enums):
ret = ''
for enumType in enumTypeOrder:
enum = None
for enumIt in enums:
if enumIt['enumType'] == enumType:
enum = enumIt
break
if not enum:
continue
typeFragment = get_fragment(enumType)
ret += '## {}\n\n'.format(enumType)
for enumIdentifier in enum['enumIdentifiers']:
enumIdentifierString = enumIdentifier['enumIdentifier']
enumIdentifierHeader = '{}::{}'.format(enumType, enumIdentifierString)
enumIdentifierFragment = get_fragment(enumIdentifierHeader, False)
ret += '### {}\n\n'.format(enumIdentifierHeader)
ret += '{}\n\n'.format(enumIdentifier['description'])
ret += '- Identifier Value: `{}`\n'.format(enumIdentifier['enumValue'])
ret += '- Latest Supported RPC Version: `{}`\n'.format(enumIdentifier['rpcVersion'])
if enumIdentifier['deprecated']:
ret += '- **⚠️ Deprecated. ⚠️**\n'
if enumIdentifier['initialVersion'].lower() == 'unreleased':
ret += '- Unreleased\n'
else:
ret += '- Added in v{}\n'.format(enumIdentifier['initialVersion'])
if enumIdentifier != enum['enumIdentifiers'][-1]:
ret += '\n---\n\n'
return ret
def get_requests_toc(requests):
ret = ''
for category in categoryOrder:
requestsOut = []
for request in requests:
if request['category'] != category.lower():
continue
requestsOut.append(request)
if not len(requestsOut):
continue
categoryFragment = get_fragment(category, False)
ret += '- [{}](#{})\n'.format(category, categoryFragment)
for request in requestsOut:
requestType = request['requestType']
requestTypeFragment = get_fragment(requestType, False)
ret += ' - [{}](#{})\n'.format(requestType, requestTypeFragment)
return ret
def get_requests(requests):
ret = ''
for category in categoryOrder:
requestsOut = []
for request in requests:
if request['category'] != category.lower():
continue
requestsOut.append(request)
if not len(requestsOut):
continue
categoryFragment = get_fragment(category)
ret += '\n\n## {}\n\n'.format(category)
for request in requestsOut:
requestType = request['requestType']
requestTypeFragment = get_fragment(requestType)
ret += '### {}\n\n'.format(requestType)
ret += '{}\n\n'.format(request['description'])
ret += '- Complexity Rating: `{}/5`\n'.format(request['complexity'])
ret += '- Latest Supported RPC Version: `{}`\n'.format(request['rpcVersion'])
if request['deprecated']:
ret += '- **⚠️ Deprecated. ⚠️**\n'
if request['initialVersion'].lower() == 'unreleased':
ret += '- Unreleased\n'
else:
ret += '- Added in v{}\n'.format(request['initialVersion'])
if request['requestFields']:
ret += requestFieldHeader
for requestField in request['requestFields']:
valueRestrictions = requestField['valueRestrictions'] if requestField['valueRestrictions'] else 'None'
valueOptional = '?' if requestField['valueOptional'] else ''
valueOptionalBehavior = requestField['valueOptionalBehavior'] if requestField['valueOptional'] and requestField['valueOptionalBehavior'] else 'N/A'
ret += '| {}{} | {} | {} | {} | {} |\n'.format(valueOptional, requestField['valueName'], requestField['valueType'], requestField['valueDescription'], valueRestrictions, valueOptionalBehavior)
if request['responseFields']:
ret += responseFieldHeader
for responseField in request['responseFields']:
ret += '| {} | {} | {} |\n'.format(responseField['valueName'], responseField['valueType'], responseField['valueDescription'])
if request != requestsOut[-1]:
ret += '\n---\n\n'
return ret
def get_events_toc(events):
ret = ''
for category in categoryOrder:
eventsOut = []
for event in events:
if event['category'] != category.lower():
continue
eventsOut.append(event)
if not len(eventsOut):
continue
categoryFragment = get_fragment(category, False)
ret += '- [{}](#{})\n'.format(category, categoryFragment)
for event in eventsOut:
eventType = event['eventType']
eventTypeFragment = get_fragment(eventType, False)
ret += ' - [{}](#{})\n'.format(eventType, eventTypeFragment)
return ret
def get_events(events):
ret = ''
for category in categoryOrder:
eventsOut = []
for event in events:
if event['category'] != category.lower():
continue
eventsOut.append(event)
if not len(eventsOut):
continue
categoryFragment = get_fragment(category)
ret += '## {}\n\n'.format(category)
for event in eventsOut:
eventType = event['eventType']
eventTypeFragment = get_fragment(eventType)
ret += '### {}\n\n'.format(eventType)
ret += '{}\n\n'.format(event['description'])
ret += '- Complexity Rating: `{}/5`\n'.format(event['complexity'])
ret += '- Latest Supported RPC Version: `{}`\n'.format(event['rpcVersion'])
if event['deprecated']:
ret += '- **⚠️ Deprecated. ⚠️**\n'
if event['initialVersion'].lower() == 'unreleased':
ret += '- Unreleased\n'
else:
ret += '- Added in v{}\n'.format(event['initialVersion'])
if event['dataFields']:
ret += dataFieldHeader
for dataField in event['dataFields']:
ret += '| {} | {} | {} |\n'.format(dataField['valueName'], dataField['valueType'], dataField['valueDescription'])
if event != eventsOut[-1]:
ret += '\n---\n\n'
return ret
# Actual code
#######################################################################################################################
# Read versions json
try:
with open('../versions.json', 'r') as f:
versions = json.load(f)
except IOError:
logging.error('Failed to get global versions. Versions file not configured?')
os.exit(1)
# Read protocol json
with open('../generated/protocol.json', 'r') as f:
protocol = json.load(f)
output = "<!-- This file was automatically generated. Do not edit directly! -->\n\n"
# Insert introduction partial
output += read_file('partials/introduction.md')
logging.info('Inserted introduction section.')
output += '\n\n'
# Generate enums MD
output += read_file('partials/enumsHeader.md')
output += get_enums_toc(protocol['enums'])
output += '\n\n'
output += get_enums(protocol['enums'])
logging.info('Inserted enums section.')
output += '\n\n'
# Generate events MD
output += read_file('partials/eventsHeader.md')
output += get_events_toc(protocol['events'])
output += '\n\n'
output += get_events(protocol['events'])
logging.info('Inserted events section.')
output += '\n\n'
# Generate requests MD
output += read_file('partials/requestsHeader.md')
output += get_requests_toc(protocol['requests'])
output += '\n\n'
output += get_requests(protocol['requests'])
logging.info('Inserted requests section.')
output += '\n\n'
# Write new protocol MD
with open('../generated/protocol.md', 'w') as f:
f.write(output)
logging.info('Finished generating protocol.md.')

View File

@ -1,2 +1,4 @@
# Enums
These are enumeration declarations, which are referenced throughout obs-websocket's protocol.
### Enumerations Table of Contents

View File

@ -0,0 +1,3 @@
# Events
### Events Table of Contents

View File

@ -1,38 +1,37 @@
# obs-websocket 5.0.0 protocol reference
# Main Table of Contents
- [obs-websocket 5.0.0 Protocol](#obs-websocket-500-protocol)
- [Connecting to obs-websocket](#connecting-to-obs-websocket)
- [Connection steps](#connection-steps)
- [Creating an authentication string](#creating-an-authentication-string)
- [Base message types](#message-types)
- [OpCode 0 Hello](#hello-opcode-0)
- [OpCode 1 Identify](#identify-opcode-1)
- [OpCode 2 Identified](#identified-opcode-2)
- [OpCode 3 Reidentify](#reidentify-opcode-3)
- [OpCode 5 Event](#event-opcode-5)
- [OpCode 6 Request](#request-opcode-6)
- [OpCode 7 RequestResponse](#requestresponse-opcode-7)
- [OpCode 8 RequestBatch](#requestbatch-opcode-8)
- [OpCode 9 RequestBatchResponse](#requestbatchresponse-opcode-9)
- [Enums](#enums)
- [Events](#events)
- [Requests](#requests)
# obs-websocket 5.0.0 Protocol
## General Introduction
## General Intro
obs-websocket provides a feature-rich RPC communication protocol, giving access to much of OBS's feature set. This document contains everything you should know in order to make a connection and use obs-websocket's functionality to the fullest.
### Design Goals
- Abstraction of identification, events, requests, and batch requests into dedicated message types
- Conformity of request naming using similar terms like `Get`, `Set`, `Get[x]List`, `Start[x]`, `Toggle[x]`
- Conformity of OBS data key names like `sourceName`, `sourceKind`, `sourceType`, `sceneName`, `sceneItemName`
- Conformity of OBS data field names like `sourceName`, `sourceKind`, `sourceType`, `sceneName`, `sceneItemName`
- Error code response system - integer corrosponds to type of error, with optional comment
- Possible support for multiple message encoding options: JSON and MessagePack
- PubSub system - Allow clients to specify which events they do or don't want to receive from OBS
- RPC versioning - Client and server negotiate the latest version of the obs-websocket protocol to communicate with.
## Table of Contents
- [Connecting to obs-websocket](#connecting-to-obs-websocket)
- [Connection steps](#connection-steps)
- [Creating an authentication string](#creating-an-authentication-string)
- [Enumerations](#enumerations)
- [Base message types](#message-types)
- [OpCode 0 Hello](#hello-opcode-0)
- [OpCode 1 Identify](#identify-opcode-1)
- [OpCode 2 Identified](#identified-opcode-2)
- [OpCode 3 Reidentify](#reidentify-opcode-3)
- [OpCode 5 Event](#event-opcode-5)
- [OpCode 6 Request](#request-opcode-6)
- [OpCode 7 RequestResponse](#requestresponse-opcode-7)
- [OpCode 8 RequestBatch](#requestbatch-opcode-8)
- [OpCode 9 RequestBatchResponse](#requestbatchresponse-opcode-9)
- [Events](#events)
- [Requests](#requests)
## Connecting to obs-websocket
Here's info on how to connect to obs-websocket
@ -49,13 +48,13 @@ These steps should be followed precisely. Failure to connect to the server as in
- Once the connection is upgraded, the websocket server will immediately send an [OpCode 0 `Hello`](#hello-opcode-0) message to the client.
- The client listens for the `Hello` and responds with an [OpCode 1 `Identify`](#identify-opcode-1) containing all appropriate session parameters.
- If there is an `authentication` key in the `messageData` object, the server requires authentication, and the steps in [Creating an authentication string](#creating-an-authentication-string) should be followed.
- If there is no `authentication` key, the resulting `Identify` object sent to the server does not require an `authentication` string.
- If there is an `authentication` field in the `messageData` object, the server requires authentication, and the steps in [Creating an authentication string](#creating-an-authentication-string) should be followed.
- If there is no `authentication` field, the resulting `Identify` object sent to the server does not require an `authentication` string.
- The client determines if the server's `rpcVersion` is supported, and if not it provides its closest supported version in `Identify`.
- The server receives and processes the `Identify` sent by the client.
- If authentication is required and the `Identify` message data does not contain an `authentication` string, or the string is not correct, the connection is closed with [`WebSocketCloseCode::AuthenticationFailed`](#websocketclosecode-enum)
- If the client has requested an `rpcVersion` which the server cannot use, the connection is closed with [`WebSocketCloseCode::UnsupportedRpcVersion`](#websocketclosecode-enum). This system allows both the server and client to have seamless backwards compatability.
- If authentication is required and the `Identify` message data does not contain an `authentication` string, or the string is not correct, the connection is closed with `WebSocketCloseCode::AuthenticationFailed`
- If the client has requested an `rpcVersion` which the server cannot use, the connection is closed with `WebSocketCloseCode::UnsupportedRpcVersion`. This system allows both the server and client to have seamless backwards compatability.
- If any other parameters are malformed (invalid type, etc), the connection is closed with an appropriate close code.
- Once identification is processed on the server, the server responds to the client with an [OpCode 2 `Identified`](#identified-opcode-2).
@ -65,15 +64,15 @@ These steps should be followed precisely. Failure to connect to the server as in
- At any time after a client has been identified, it may send an [OpCode 3 `Reidentify`](#reidentify-opcode-3) message to update certain allowed session parameters. The server will respond in the same way it does during initial identification.
#### Connection Notes
- If a binary frame is received when using the `obswebsocket.json` (default) subprotocol, or a text frame is received while using the `obswebsocket.msgpack` subprotocol, the connection is closed with [`WebSocketCloseCode::MessageDecodeError`](#websocketclosecode-enum).
- The obs-websocket server listens for any messages containing a `request-type` key in the first level JSON from unidentified clients. If a message matches, the connection is closed with [`WebSocketCloseCode::UnsupportedRpcVersion`](#websocketclosecode-enum) and a warning is logged.
- If a message with a `messageType` is not recognized to the obs-websocket server, the connection is closed with [`WebSocketCloseCode::UnknownOpCode`](#websocketclosecode-enum).
- At no point may the client send any message other than a single `Identify` before it has received an `Identified`. Doing so will result in the connection being closed with [`WebSocketCloseCode::NotIdentified`](#websocketclosecode-enum).
- If a binary frame is received when using the `obswebsocket.json` (default) subprotocol, or a text frame is received while using the `obswebsocket.msgpack` subprotocol, the connection is closed with `WebSocketCloseCode::MessageDecodeError`.
- The obs-websocket server listens for any messages containing a `request-type` field in the first level JSON from unidentified clients. If a message matches, the connection is closed with `WebSocketCloseCode::UnsupportedRpcVersion` and a warning is logged.
- If a message with a `messageType` is not recognized to the obs-websocket server, the connection is closed with `WebSocketCloseCode::UnknownOpCode`.
- At no point may the client send any message other than a single `Identify` before it has received an `Identified`. Doing so will result in the connection being closed with `WebSocketCloseCode::NotIdentified`.
---
### Creating an authentication string
obs-websocket uses SHA256 to transmit authentication credentials. The server starts by sending an object in the `authentication` key of its `Hello` message data. The client processes the authentication challenge and responds via the `authentication` string in the `Identify` message data.
obs-websocket uses SHA256 to transmit authentication credentials. The server starts by sending an object in the `authentication` field of its `Hello` message data. The client processes the authentication challenge and responds via the `authentication` string in the `Identify` message data.
For this guide, we'll be using `supersecretpassword` as the password.
@ -93,172 +92,19 @@ To generate the authentication string, follow these steps:
For real-world examples of the `authentication` string creation, refer to the obs-websocket client libraries listed on the [README](README.md).
---
### Enumerations
These are the enumeration definitions for various codes used by obs-websocket.
#### WebSocketOpCode Enum
```cpp
enum WebSocketOpCode {
Hello = 0,
Identify = 1,
Identified = 2,
Reidentify = 3,
Event = 5,
Request = 6,
RequestResponse = 7,
RequestBatch = 8,
RequestBatchResponse = 9,
};
```
#### WebSocketCloseCode Enum
```cpp
enum WebSocketCloseCode {
// Internal only
DontClose = 0,
// Reserved
UnknownReason = 4000,
// The server was unable to decode the incoming websocket message
MessageDecodeError = 4002,
// A data key is missing but required
MissingDataKey = 4003,
// A data key has an invalid type
InvalidDataKeyType = 4004,
// The specified `op` was invalid or missing
UnknownOpCode = 4005,
// The client sent a websocket message without first sending `Identify` message
NotIdentified = 4006,
// The client sent an `Identify` message while already identified
AlreadyIdentified = 4007,
// The authentication attempt (via `Identify`) failed
AuthenticationFailed = 4008,
// The server detected the usage of an old version of the obs-websocket protocol.
UnsupportedRpcVersion = 4009,
// The websocket session has been invalidated by the obs-websocket server.
SessionInvalidated = 4010,
};
```
#### EventSubscriptions Enum
```cpp
enum EventSubscription {
// Set subscriptions to 0 to disable all events
None = 0,
// Receive events in the `General` category
General = (1 << 0),
// Receive events in the `Config` category
Config = (1 << 1),
// Receive events in the `Scenes` category
Scenes = (1 << 2),
// Receive events in the `Inputs` category
Inputs = (1 << 3),
// Receive events in the `Transitions` category
Transitions = (1 << 4),
// Receive events in the `Filters` category
Filters = (1 << 5),
// Receive events in the `Outputs` category
Outputs = (1 << 6),
// Receive events in the `Scene Items` category
SceneItems = (1 << 7),
// Receive events in the `MediaInputs` category
MediaInputs = (1 << 8),
// Receive all event categories
All = (General | Config | Scenes | Inputs | Transitions | Filters | Outputs | SceneItems | MediaInputs),
// InputVolumeMeters event (high-volume)
InputVolumeMeters = (1 << 9),
// InputActiveStateChanged event (high-volume)
InputActiveStateChanged = (1 << 10),
// InputShowStateChanged event (high-volume)
InputShowStateChanged = (1 << 11),
};
```
Subscriptions are a bitmask system. In many languages, to generate a bitmask that subscribes to `General` and `Scenes`, you would do: `subscriptions = ((1 << 0) | (1 << 2))`
#### RequestStatus Enum
```cpp
enum RequestStatus {
Unknown = 0,
// For internal use to signify a successful parameter check
NoError = 10,
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 (comment required)
GenericError = 205,
// A required request parameter is missing
MissingRequestParameter = 300,
// The request does not have a valid requestData object.
MissingRequestData = 301,
// Generic invalid request parameter message (comment required)
InvalidRequestParameter = 400,
// A request parameter has the wrong data type
InvalidRequestParameterType = 401,
// A request parameter (float or int) is out of valid range
RequestParameterOutOfRange = 402,
// A request parameter (string or array) is empty and cannot be
RequestParameterEmpty = 403,
// There are too many request parameters (eg. a request takes two optionals, where only one is allowed at a time)
TooManyRequestParameters = 404,
// An output is running and cannot be in order to perform the request (generic)
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 disabled and should not be
OutputDisabled = 503,
// Studio mode is active and cannot be
StudioModeActive = 504,
// Studio mode is not active and should be
StudioModeNotActive = 505,
// The resource was not found
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,
// Creating the resource failed
ResourceCreationFailed = 700,
// Performing an action on the resource failed
ResourceActionFailed = 701,
// Processing the request failed unexpectedly (comment required)
RequestProcessingFailed = 702,
// The combination of request parameters cannot be used to perform an action
CannotAct = 703,
};
```
## Message Types (OpCodes)
The following message types are the low-level message types which may be sent to and from obs-websocket.
Messages sent from the obs-websocket server or client may contain these first-level keys, known as the base object:
Messages sent from the obs-websocket server or client may contain these first-level fields, known as the base object:
```
{
"op": number,
"d": object
}
```
- `op` is a [`WebSocketOpCode` OpCode.](#websocketopcode-enum)
- `d` is an object of the data keys associated with the operation.
- `op` is a `WebSocketOpCode` OpCode.
- `d` is an object of the data fields associated with the operation.
---
@ -321,8 +167,8 @@ Authentication is not required
}
```
- `rpcVersion` is the version number that the client would like the obs-websocket server to use.
- When `ignoreInvalidMessages` is true, the socket will not be closed for [`WebSocketCloseCode`](#websocketclosecode-enum): `MessageDecodeError`, `UnknownOpCode`, or `MissingDataKey`. Instead, the message will be logged and ignored.
- `eventSubscriptions` is a bitmask of [`EventSubscriptions`](#eventsubscriptions-enum) items to subscribe to events and event categories at will. By default, all event categories are subscribed, except for events marked as high volume. High volume events must be explicitly subscribed to.
- When `ignoreInvalidMessages` is true, the socket will not be closed for `WebSocketCloseCode`: `MessageDecodeError`, `UnknownOpCode`, or `MissingDataKey`. Instead, the message will be logged and ignored.
- `eventSubscriptions` is a bitmask of `EventSubscriptions` items to subscribe to events and event categories at will. By default, all event categories are subscribed, except for events marked as high volume. High volume events must be explicitly subscribed to.
**Example Message:**
```json
@ -465,8 +311,8 @@ Authentication is not required
"comment": string(optional)
}
```
- `result` is `true` if the request resulted in [`RequestStatus::Success`](#requeststatus-enum). False if otherwise.
- `code` is a [`RequestStatus`](#requeststatus-enum) code.
- `result` is `true` if the request resulted in `RequestStatus::Success`. False if otherwise.
- `code` is a `RequestStatus` code.
- `comment` may be provided by the server on errors to offer further details on why a request failed.
**Example Messages:**
@ -513,11 +359,12 @@ Failure Response
{
"requestId": string,
"haltOnFailure": bool(optional) = false,
"executionType": number(optional) = RequestBatchExecutionType::SerialRealtime
"requests": array<object>
}
```
- When `haltOnFailure` is `true`, the processing of requests will be halted on first failure. Returns only the processed requests in [`RequestBatchResponse`](#requestbatchresponse-opcode-9).
- Requests in the `requests` array follow the same structure as the `Request` payload data format, however `requestId` is an optional key.
- Requests in the `requests` array follow the same structure as the `Request` payload data format, however `requestId` is an optional field.
---

View File

@ -0,0 +1,3 @@
# Requests
### Requests Table of Contents

View File

@ -0,0 +1,208 @@
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s [process_comments.py] [%(levelname)s] %(message)s")
import os
import sys
import json
# The comments parser will return a string type instead of an array if there is only one field
def field_to_array(field):
if type(field) == str:
return [field]
return field
# This raw JSON can be really damn unpredictable. Let's handle that
def field_to_string(field):
if type(field) == list:
return field_to_string(field[0])
elif type(field) == dict:
return field_to_string(field['description'])
return str(field)
# Make sure that everything we expect is there
def validate_fields(data, fields):
for field in fields:
if field not in data:
logging.warning('Missing required item: {}'.format(field))
return False
return True
# Get the individual components of a `requestField` or `responseField` or `dataField` entry
def get_components(data):
ret = []
components_raw = data.split('|')
for component in components_raw:
ret.append(component.strip())
return ret
# Convert all request fields from raw to final
def get_request_fields(fields):
fields = field_to_array(fields)
ret = []
for field in fields:
components = get_components(field)
field_out = {}
field_out['valueName'] = components[0].replace('?', '')
field_out['valueType'] = components[1]
field_out['valueDescription'] = components[2]
valueOptionalOffset = 3
# If value type is a number, restrictions are required. Else, should not be added.
if field_out['valueType'].lower() == 'number':
# In the case of a number, the optional component gets pushed back.
valueOptionalOffset += 1
field_out['valueRestrictions'] = components[3] if components[3].lower() != 'none' else None
else:
field_out['valueRestrictions'] = None
field_out['valueOptional'] = components[0].startswith('?')
if field_out['valueOptional']:
field_out['valueOptionalBehavior'] = components[valueOptionalOffset] if len(components) > valueOptionalOffset else 'Unknown'
else:
field_out['valueOptionalBehavior'] = None
ret.append(field_out)
return ret
# Convert all response (or event data) fields from raw to final
def get_response_fields(fields):
fields = field_to_array(fields)
ret = []
for field in fields:
components = get_components(field)
field_out = {}
field_out['valueName'] = components[0]
field_out['valueType'] = components[1]
field_out['valueDescription'] = components[2]
ret.append(field_out)
return ret
#######################################################################################################################
# Read versions json
try:
with open('../versions.json', 'r') as f:
versions = json.load(f)
except IOError:
logging.error('Failed to get global versions. Versions file not configured?')
os.exit(1)
# Read the raw comments output file
with open('../work/comments.json', 'r') as f:
comments_raw = json.load(f)
# Prepare output variables
enums = []
requests = []
events = []
enums_raw = {}
# Process the raw comments
for comment in comments_raw:
# Skip unrelated comments like #include
if 'api' not in comment:
continue
api = comment['api']
if api == 'enums':
if not validate_fields(comment, ['description', 'enumIdentifier', 'enumType', 'rpcVersion', 'initialVersion']):
logging.warning('Failed to process enum id comment due to missing field(s):\n{}'.format(comment))
continue
enumType = field_to_string(comment['enumType'])
enum = {}
# Recombines the header back into one string, allowing multi-line descriptions.
enum['description'] = field_to_string(comment.get('lead', '')) + field_to_string(comment['description'])
enum['enumIdentifier'] = field_to_string(comment['enumIdentifier'])
rpcVersionRaw = field_to_string(comment['rpcVersion'])
enum['rpcVersion'] = versions['rpcVersion'] if rpcVersionRaw == '-1' else int(rpcVersionRaw)
enum['deprecated'] = False if rpcVersionRaw == '-1' else True
enum['initialVersion'] = field_to_string(comment['initialVersion'])
if 'enumValue' in comment:
enumValue = field_to_string(comment['enumValue'])
enum['enumValue'] = int(enumValue) if enumValue.isdigit() else enumValue
else:
enum['enumValue'] = None
if enumType not in enums_raw:
enums_raw[enumType] = {'enumIdentifiers': [enum]}
else:
enums_raw[enumType]['enumIdentifiers'].append(enum)
logging.info('Processed enum: {}::{}'.format(enumType, enum['enumIdentifier']))
elif api == 'requests':
if not validate_fields(comment, ['description', 'requestType', 'complexity', 'rpcVersion', 'initialVersion', 'category']):
logging.warning('Failed to process request comment due to missing field(s):\n{}'.format(comment))
continue
req = {}
req['description'] = field_to_string(comment.get('lead', '')) + field_to_string(comment['description'])
req['requestType'] = field_to_string(comment['requestType'])
req['complexity'] = int(field_to_string(comment['complexity']))
rpcVersionRaw = field_to_string(comment['rpcVersion'])
req['rpcVersion'] = versions['rpcVersion'] if rpcVersionRaw == '-1' else int(rpcVersionRaw)
req['deprecated'] = False if rpcVersionRaw == '-1' else True
req['initialVersion'] = field_to_string(comment['initialVersion'])
req['category'] = field_to_string(comment['category'])
try:
if 'requestField' in comment:
req['requestFields'] = get_request_fields(comment['requestField'])
else:
req['requestFields'] = []
except:
logging.exception('Failed to process request `{}` request fields due to error:\n'.format(req['requestType']))
continue
try:
if 'responseField' in comment:
req['responseFields'] = get_response_fields(comment['responseField'])
else:
req['responseFields'] = []
except:
logging.exception('Failed to process request `{}` request fields due to error:\n'.format(req['requestType']))
continue
logging.info('Processed request: {}'.format(req['requestType']))
requests.append(req)
elif api == 'events':
if not validate_fields(comment, ['description', 'eventType', 'eventSubscription', 'complexity', 'rpcVersion', 'initialVersion', 'category']):
logging.warning('Failed to process event comment due to missing field(s):\n{}'.format(comment))
continue
eve = {}
eve['description'] = field_to_string(comment.get('lead', '')) + field_to_string(comment['description'])
eve['eventType'] = field_to_string(comment['eventType'])
eve['eventSubscription'] = field_to_string(comment['eventSubscription'])
eve['complexity'] = int(field_to_string(comment['complexity']))
rpcVersionRaw = field_to_string(comment['rpcVersion'])
eve['rpcVersion'] = versions['rpcVersion'] if rpcVersionRaw == '-1' else int(rpcVersionRaw)
eve['deprecated'] = False if rpcVersionRaw == '-1' else True
eve['initialVersion'] = field_to_string(comment['initialVersion'])
eve['category'] = field_to_string(comment['category'])
try:
if 'dataField' in comment:
eve['dataFields'] = get_response_fields(comment['dataField'])
else:
eve['dataFields'] = []
except:
logging.exception('Failed to process event `{}` data fields due to error:\n'.format(req['eventType']))
continue
logging.info('Processed event: {}'.format(eve['eventType']))
events.append(eve)
else:
logging.warning('Comment with unknown api: {}'.format(api))
# Reconfigure enums to match the correct structure
for enumType in enums_raw.keys():
enum = enums_raw[enumType]
enums.append({'enumType': enumType, 'enumIdentifiers': enum['enumIdentifiers']})
finalObject = {'enums': enums, 'requests': requests, 'events': events}
with open('../generated/protocol.json', 'w') as f:
json.dump(finalObject, f, indent=2)

View File

@ -1 +0,0 @@
{}

2353
docs/generated/protocol.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +0,0 @@
{
"name": "obs-websocket-docs",
"version": "1.1.0",
"description": "",
"main": "docs.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"docs": "node ./docs.js",
"comments": "node ./comments.js",
"build": "npm run comments && npm run docs"
},
"author": "",
"license": "ISC",
"dependencies": {
"glob": "^7.1.2",
"handlebars": "^4.0.10",
"handlebars-helpers": "^0.9.6",
"markdown-toc": "^1.1.0",
"parse-comments": "^0.4.3"
}
}

View File

@ -1 +0,0 @@
## Events

View File

@ -1 +0,0 @@
## Requests

View File

@ -1,98 +0,0 @@
{{#read "partials/introduction.md"}}{{/read}}
## Requests/Events Table of Contents
<!-- toc -->
{{#read "partials/eventsHeader.md"}}{{/read}}
{{#each events}}
## {{capitalizeAll @key}}
{{#each this}}
### {{name}}
{{#if deprecated}}
- **⚠️ Deprecated. Last seen in RPC v{{deprecated}} ⚠️**
{{/if}}
{{#eq since "unreleased"}}
- Unreleased
{{else}}
- Added in v{{since}}
{{/eq}}
{{{description}}}
**Response Items:**
{{#if returns.length}}
| Name | Type | Description |
| ---- | :---: | ------------|
{{#each returns}}
| `{{name}}` | _{{depipe type}}_ | {{{depipe description}}} |
{{/each}}
{{else}}
_No additional response items._
{{/if}}
---
{{/each}}
{{/each}}
{{#read "partials/requestsHeader.md"}}{{/read}}
{{#each requests}}
## {{capitalizeAll @key}}
{{#each this}}
### {{name}}
{{#if deprecated}}
- **⚠️ Deprecated. Last seen in RPC v{{deprecated}} ⚠️**
{{/if}}
{{#eq since "unreleased"}}
- Unreleased
{{else}}
- Added in v{{since}}
{{/eq}}
{{{description}}}
**Request Fields:**
{{#if params.length}}
| Name | Type | Description |
| ---- | :---: | ------------|
{{#each params}}
| `{{name}}` | _{{depipe type}}_ | {{{depipe description}}} |
{{/each}}
{{else}}
_No specified parameters._
{{/if}}
**Response Items:**
{{#if returns.length}}
| Name | Type | Description |
| ---- | :---: | ------------|
{{#each returns}}
| `{{name}}` | _{{depipe type}}_ | {{{depipe description}}} |
{{/each}}
{{else}}
_No additional response items._
{{/if}}
---
{{/each}}
{{/each}}

5
docs/versions.json Normal file
View File

@ -0,0 +1,5 @@
{
"obsWebSocketProjectVersion": "5.0.0",
"obsWebSocketVersion": "5.0.0-alpha2",
"rpcVersion": "1"
}

View File

@ -19,6 +19,22 @@ with this program. If not, see <https://www.gnu.org/licenses/>
#include "EventHandler.h"
/**
* The current scene collection has begun changing.
*
* Note: We recommend using this event to trigger a pause of all polling requests, as performing any requests during a
* scene collection change is considered undefined behavior and can cause crashes!
*
* @dataField sceneCollectionName | String | Name of the current scene collection
*
* @eventType CurrentSceneCollectionChanging
* @eventSubscription Config
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api events
*/
void EventHandler::HandleCurrentSceneCollectionChanging()
{
json eventData;
@ -26,6 +42,21 @@ void EventHandler::HandleCurrentSceneCollectionChanging()
BroadcastEvent(EventSubscription::Config, "CurrentSceneCollectionChanging", eventData);
}
/**
* The current scene collection has changed.
*
* Note: If polling has been paused during `CurrentSceneCollectionChanging`, this is the que to restart polling.
*
* @dataField sceneCollectionName | String | Name of the new scene collection
*
* @eventType CurrentSceneCollectionChanged
* @eventSubscription Config
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api events
*/
void EventHandler::HandleCurrentSceneCollectionChanged()
{
json eventData;
@ -33,6 +64,19 @@ void EventHandler::HandleCurrentSceneCollectionChanged()
BroadcastEvent(EventSubscription::Config, "CurrentSceneCollectionChanged", eventData);
}
/**
* The scene collection list has changed.
*
* @dataField sceneCollections | Array<String> | Updated list of scene collections
*
* @eventType SceneCollectionListChanged
* @eventSubscription Config
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api events
*/
void EventHandler::HandleSceneCollectionListChanged()
{
json eventData;
@ -40,6 +84,19 @@ void EventHandler::HandleSceneCollectionListChanged()
BroadcastEvent(EventSubscription::Config, "SceneCollectionListChanged", eventData);
}
/**
* The current profile has begun changing.
*
* @dataField profileName | String | Name of the current profile
*
* @eventType CurrentProfileChanging
* @eventSubscription Config
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api events
*/
void EventHandler::HandleCurrentProfileChanging()
{
json eventData;
@ -47,6 +104,19 @@ void EventHandler::HandleCurrentProfileChanging()
BroadcastEvent(EventSubscription::Config, "CurrentProfileChanging", eventData);
}
/**
* The current profile has changed.
*
* @dataField profileName | String | Name of the new profile
*
* @eventType CurrentProfileChanged
* @eventSubscription Config
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api events
*/
void EventHandler::HandleCurrentProfileChanged()
{
json eventData;
@ -54,6 +124,19 @@ void EventHandler::HandleCurrentProfileChanged()
BroadcastEvent(EventSubscription::Config, "CurrentProfileChanged", eventData);
}
/**
* The profile list has changed.
*
* @dataField profiles | Array<String> | Updated list of profiles
*
* @eventType ProfileListChanged
* @eventSubscription Config
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api events
*/
void EventHandler::HandleProfileListChanged()
{
json eventData;

View File

@ -19,11 +19,35 @@ with this program. If not, see <https://www.gnu.org/licenses/>
#include "EventHandler.h"
/**
* OBS has begun the shutdown process.
*
* @eventType ExitStarted
* @eventSubscription General
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category general
* @api events
*/
void EventHandler::HandleExitStarted()
{
BroadcastEvent(EventSubscription::General, "ExitStarted");
}
/**
* Studio mode has been enabled or disabled.
*
* @dataField studioModeEnabled | Boolean | True == Enabled, False == Disabled
*
* @eventType StudioModeStateChanged
* @eventSubscription General
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category general
* @api events
*/
void EventHandler::HandleStudioModeStateChanged(bool enabled)
{
json eventData;

View File

@ -21,38 +21,188 @@ with this program. If not, see <https://www.gnu.org/licenses/>
namespace EventSubscription {
enum EventSubscription {
// Set subscriptions to 0 to disable all events
/**
* Subcription value used to disable all events.
*
* @enumIdentifier None
* @enumValue 0
* @enumType EventSubscription
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
None = 0,
// Receive events in the `General` category
/**
* Subscription value to receive events in the `General` category.
*
* @enumIdentifier General
* @enumValue (1 << 0)
* @enumType EventSubscription
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
General = (1 << 0),
// Receive events in the `Config` category
/**
* Subscription value to receive events in the `Config` category.
*
* @enumIdentifier Config
* @enumValue (1 << 1)
* @enumType EventSubscription
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
Config = (1 << 1),
// Receive events in the `Scenes` category
/**
* Subscription value to receive events in the `Scenes` category.
*
* @enumIdentifier Scenes
* @enumValue (1 << 2)
* @enumType EventSubscription
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
Scenes = (1 << 2),
// Receive events in the `Inputs` category
/**
* Subscription value to receive events in the `Inputs` category.
*
* @enumIdentifier Inputs
* @enumValue (1 << 3)
* @enumType EventSubscription
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
Inputs = (1 << 3),
// Receive events in the `Transitions` category
/**
* Subscription value to receive events in the `Transitions` category.
*
* @enumIdentifier Transitions
* @enumValue (1 << 4)
* @enumType EventSubscription
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
Transitions = (1 << 4),
// Receive events in the `Filters` category
/**
* Subscription value to receive events in the `Filters` category.
*
* @enumIdentifier Filters
* @enumValue (1 << 5)
* @enumType EventSubscription
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
Filters = (1 << 5),
// Receive events in the `Outputs` category
/**
* Subscription value to receive events in the `Outputs` category.
*
* @enumIdentifier Outputs
* @enumValue (1 << 6)
* @enumType EventSubscription
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
Outputs = (1 << 6),
// Receive events in the `Scene Items` category
/**
* Subscription value to receive events in the `SceneItems` category.
*
* @enumIdentifier SceneItems
* @enumValue (1 << 7)
* @enumType EventSubscription
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
SceneItems = (1 << 7),
/**
* Subscription value to receive events in the `MediaInputs` category.
*
* @enumIdentifier MediaInputs
* @enumValue (1 << 8)
* @enumType EventSubscription
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
// Receive events in the `MediaInputs` category
MediaInputs = (1 << 8),
// InputVolumeMeters event (high-volume)
InputVolumeMeters = (1 << 9),
// InputActiveStateChanged event (high-volume)
InputActiveStateChanged = (1 << 10),
// InputShowStateChanged event (high-volume)
InputShowStateChanged = (1 << 11),
// SceneItemTransformChanged event (high-volume)
SceneItemTransformChanged = (1 << 12),
/**
* Subscription value to receive the `ExternalPluginEvent` event.
*
* @enumIdentifier ExternalPlugins
* @enumValue (1 << 9)
* @enumType EventSubscription
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
// Receive events from external OBS plugins
ExternalPlugins = (1 << 13),
ExternalPlugins = (1 << 9),
/**
* Helper to receive all non-high-volume events.
*
* @enumIdentifier All
* @enumValue (General | Config | Scenes | Inputs | Transitions | Filters | Outputs | SceneItems | MediaInputs | ExternalPlugins)
* @enumType EventSubscription
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
// Receive all event categories (exclude high-volume)
All = (General | Config | Scenes | Inputs | Transitions | Filters | Outputs | SceneItems | MediaInputs | ExternalPlugins),
/**
* Subscription value to receive the `InputVolumeMeters` high-volume event.
*
* @enumIdentifier InputVolumeMeters
* @enumValue (1 << 16)
* @enumType EventSubscription
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
// InputVolumeMeters event (high-volume)
InputVolumeMeters = (1 << 16),
/**
* Subscription value to receive the `InputActiveStateChanged` high-volume event.
*
* @enumIdentifier InputActiveStateChanged
* @enumValue (1 << 17)
* @enumType EventSubscription
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
// InputActiveStateChanged event (high-volume)
InputActiveStateChanged = (1 << 17),
/**
* Subscription value to receive the `InputShowStateChanged` high-volume event.
*
* @enumIdentifier InputShowStateChanged
* @enumValue (1 << 18)
* @enumType EventSubscription
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
// InputShowStateChanged event (high-volume)
InputShowStateChanged = (1 << 18),
/**
* Subscription value to receive the `SceneItemTransformChanged` high-volume event.
*
* @enumIdentifier SceneItemTransformChanged
* @enumValue (1 << 19)
* @enumType EventSubscription
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
// SceneItemTransformChanged event (high-volume)
SceneItemTransformChanged = (1 << 19),
};
}

View File

@ -132,7 +132,7 @@ RequestHandler::RequestHandler(SessionPtr session) :
RequestResult RequestHandler::ProcessRequest(const Request& request)
{
if (!request.RequestData.is_object() && !request.RequestData.is_null())
return RequestResult::Error(RequestStatus::InvalidRequestParameterType, "Your request data is not an object.");
return RequestResult::Error(RequestStatus::InvalidRequestFieldType, "Your request data is not an object.");
if (request.RequestType.empty())
return RequestResult::Error(RequestStatus::MissingRequestType, "Your request is missing a `requestType`");

View File

@ -26,6 +26,7 @@ with this program. If not, see <https://www.gnu.org/licenses/>
#include "rpc/Request.h"
#include "rpc/RequestResult.h"
#include "types/RequestStatus.h"
#include "types/RequestBatchExecutionType.h"
#include "../websocketserver/rpc/WebSocketSession.h"
#include "../obs-websocket.h"
#include "../utils/Obs.h"

View File

@ -22,6 +22,21 @@ with this program. If not, see <https://www.gnu.org/licenses/>
#include "RequestHandler.h"
/**
* Gets the value of a "slot" from the selected persistent data realm.
*
* @requestField realm | String | The data realm to select. `OBS_WEBSOCKET_DATA_REALM_GLOBAL` or `OBS_WEBSOCKET_DATA_REALM_PROFILE`
* @requestField slotName | String | The name of the slot to retrieve data from
*
* @responseField slotValue | String | Value associated with the slot. `null` if not set
*
* @requestType GetPersistentData
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api requests
*/
RequestResult RequestHandler::GetPersistentData(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -50,6 +65,20 @@ RequestResult RequestHandler::GetPersistentData(const Request& request)
return RequestResult::Success(responseData);
}
/**
* Sets the value of a "slot" from the selected persistent data realm.
*
* @requestField realm | String | The data realm to select. `OBS_WEBSOCKET_DATA_REALM_GLOBAL` or `OBS_WEBSOCKET_DATA_REALM_PROFILE`
* @requestField slotName | String | The name of the slot to retrieve data from
* @requestField slotValue | Any | The value to apply to the slot
*
* @requestType SetPersistentData
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api requests
*/
RequestResult RequestHandler::SetPersistentData(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -78,6 +107,19 @@ RequestResult RequestHandler::SetPersistentData(const Request& request)
return RequestResult::Success();
}
/**
* Gets an array of all scene collections
*
* @responseField currentSceneCollectionName | String | The name of the current scene collection
* @responseField sceneCollections | Array<String> | Array of all available scene collections
*
* @requestType GetSceneCollectionList
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api requests
*/
RequestResult RequestHandler::GetSceneCollectionList(const Request&)
{
json responseData;
@ -86,7 +128,20 @@ RequestResult RequestHandler::GetSceneCollectionList(const Request&)
return RequestResult::Success(responseData);
}
// Does not return until collection has finished switching
/**
* Switches to a scene collection.
*
* Note: This will block until the collection has finished changing.
*
* @requestField sceneCollectionName | String | Name of the scene collection to switch to
*
* @requestType SetCurrentSceneCollection
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api requests
*/
RequestResult RequestHandler::SetCurrentSceneCollection(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -111,6 +166,20 @@ RequestResult RequestHandler::SetCurrentSceneCollection(const Request& request)
return RequestResult::Success();
}
/**
* Creates a new scene collection, switching to it in the process.
*
* Note: This will block until the collection has finished changing.
*
* @requestField sceneCollectionName | String | Name for the new scene collection
*
* @requestType CreateSceneCollection
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api requests
*/
RequestResult RequestHandler::CreateSceneCollection(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -133,6 +202,19 @@ RequestResult RequestHandler::CreateSceneCollection(const Request& request)
return RequestResult::Success();
}
/**
* Gets an array of all profiles
*
* @responseField currentProfileName | String | The name of the current profile
* @responseField profiles | Array<String> | Array of all available profiles
*
* @requestType GetProfileList
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api requests
*/
RequestResult RequestHandler::GetProfileList(const Request&)
{
json responseData;
@ -141,6 +223,18 @@ RequestResult RequestHandler::GetProfileList(const Request&)
return RequestResult::Success(responseData);
}
/**
* Switches to a profile.
*
* @requestField profileName | String | Name of the profile to switch to
*
* @requestType SetCurrentProfile
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api requests
*/
RequestResult RequestHandler::SetCurrentProfile(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -165,6 +259,18 @@ RequestResult RequestHandler::SetCurrentProfile(const Request& request)
return RequestResult::Success();
}
/**
* Creates a new profile, switching to it in the process
*
* @requestField profileName | String | Name for the new profile
*
* @requestType CreateProfile
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api requests
*/
RequestResult RequestHandler::CreateProfile(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -184,6 +290,18 @@ RequestResult RequestHandler::CreateProfile(const Request& request)
return RequestResult::Success();
}
/**
* Removes a profile. If the current profile is chosen, it will change to a different profile first.
*
* @requestField profileName | String | Name of the profile to remove
*
* @requestType RemoveProfile
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api requests
*/
RequestResult RequestHandler::RemoveProfile(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -206,6 +324,22 @@ RequestResult RequestHandler::RemoveProfile(const Request& request)
return RequestResult::Success();
}
/**
* Gets a parameter from the current profile's configuration.
*
* @requestField parameterCategory | String | Category of the parameter to get
* @requestField parameterName | String | Name of the parameter to get
*
* @responseField parameterValue | String | Value associated with the parameter. `null` if not set and no default
* @responseField defaultParameterValue | String | Default value associated with the parameter. `null` if no default
*
* @requestType GetProfileParameter
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api requests
*/
RequestResult RequestHandler::GetProfileParameter(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -236,6 +370,20 @@ RequestResult RequestHandler::GetProfileParameter(const Request& request)
return RequestResult::Success(responseData);
}
/**
* Sets the value of a parameter in the current profile's configuration.
*
* @requestField parameterCategory | String | Category of the parameter to set
* @requestField parameterName | String | Name of the parameter to set
* @requestField parameterValue | String | Value of the parameter to set. Use `null` to delete
*
* @requestType SetProfileParameter
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api requests
*/
RequestResult RequestHandler::SetProfileParameter(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -257,12 +405,31 @@ RequestResult RequestHandler::SetProfileParameter(const Request& request)
std::string parameterValue = request.RequestData["parameterValue"];
config_set_string(profile, parameterCategory.c_str(), parameterName.c_str(), parameterValue.c_str());
} else {
return RequestResult::Error(RequestStatus::InvalidRequestParameterType, "The parameter `parameterValue` must be a string.");
return RequestResult::Error(RequestStatus::InvalidRequestFieldType, "The field `parameterValue` must be a string.");
}
return RequestResult::Success();
}
/**
* Gets the current video settings.
*
* Note: To get the true FPS value, divide the FPS numerator by the FPS denominator. Example: `60000/1001`
*
* @responseField fpsNumerator | Number | Numerator of the fractional FPS value
* @responseField fpsDenominator | Number | Denominator of the fractional FPS value
* @responseField baseWidth | Number | Width of the base (canvas) resolution in pixels
* @responseField baseHeight | Number | Height of the base (canvas) resolution in pixels
* @responseField outputWidth | Number | Width of the output resolution in pixels
* @responseField outputHeight | Number | Height of the output resolution in pixels
*
* @requestType GetVideoSettings
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api requests
*/
RequestResult RequestHandler::GetVideoSettings(const Request&)
{
struct obs_video_info ovi;
@ -280,6 +447,25 @@ RequestResult RequestHandler::GetVideoSettings(const Request&)
return RequestResult::Success(responseData);
}
/**
* Sets the current video settings.
*
* Note: Fields must be specified in pairs. For example, you cannot set only `baseWidth` without needing to specify `baseHeight`.
*
* @requestField ?fpsNumerator | Number | Numerator of the fractional FPS value | >= 1 | Not changed
* @requestField ?fpsDenominator | Number | Denominator of the fractional FPS value | >= 1 | Not changed
* @requestField ?baseWidth | Number | Width of the base (canvas) resolution in pixels | >= 1, <= 4096 | Not changed
* @requestField ?baseHeight | Number | Height of the base (canvas) resolution in pixels | >= 1, <= 4096 | Not changed
* @requestField ?outputWidth | Number | Width of the output resolution in pixels | >= 1, <= 4096 | Not changed
* @requestField ?outputHeight | Number | Height of the output resolution in pixels | >= 1, <= 4096 | Not changed
*
* @requestType SetVideoSettings
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api requests
*/
RequestResult RequestHandler::SetVideoSettings(const Request& request)
{
if (obs_video_active())
@ -323,9 +509,22 @@ RequestResult RequestHandler::SetVideoSettings(const Request& request)
return RequestResult::Success();
}
return RequestResult::Error(RequestStatus::MissingRequestParameter, "You must specify at least one video-changing pair.");
return RequestResult::Error(RequestStatus::MissingRequestField, "You must specify at least one video-changing pair.");
}
/**
* Gets the current stream service settings (stream destination).
*
* @responseField streamServiceType | String | Stream service type, like `rtmp_custom` or `rtmp_common`
* @responseField streamServiceSettings | Object | Stream service settings
*
* @requestType GetStreamServiceSettings
* @complexity 4
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api requests
*/
RequestResult RequestHandler::GetStreamServiceSettings(const Request&)
{
json responseData;
@ -338,6 +537,21 @@ RequestResult RequestHandler::GetStreamServiceSettings(const Request&)
return RequestResult::Success(responseData);
}
/**
* Sets the current stream service settings (stream destination).
*
* Note: Simple RTMP settings can be set with type `rtmp_custom` and the settings fields `server` and `key`.
*
* @requestField streamServiceType | String | Type of stream service to apply. Example: `rtmp_common` or `rtmp_custom`
* @requestField streamServiceSettings | Object | Settings to apply to the service
*
* @requestType SetStreamServiceSettings
* @complexity 4
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api requests
*/
RequestResult RequestHandler::SetStreamServiceSettings(const Request& request)
{
if (obs_frontend_streaming_active())

View File

@ -24,6 +24,22 @@ with this program. If not, see <https://www.gnu.org/licenses/>
#include "../eventhandler/types/EventSubscription.h"
#include "../obs-websocket.h"
/**
* Gets data about the current plugin and RPC version.
*
* @responseField obsVersion | String | Current OBS Studio version
* @responseField obsWebSocketVersion | String | Current obs-websocket version
* @responseField rpcVersion | Number | Current latest obs-websocket RPC version
* @responseField availableRequests | Array<String> | Array of available RPC requests for the currently negotiated RPC version
*
* @requestType GetVersion
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category general
* @api requests
*/
RequestResult RequestHandler::GetVersion(const Request&)
{
json responseData;
@ -42,6 +58,18 @@ RequestResult RequestHandler::GetVersion(const Request&)
return RequestResult::Success(responseData);
}
/**
* Broadcasts a `CustomEvent` to all WebSocket clients. Receivers are clients which are identified and subscribed.
*
* @requestField eventData | Object | Data payload to emit to all receivers
*
* @requestType BroadcastCustomEvent
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category general
* @api requests
*/
RequestResult RequestHandler::BroadcastCustomEvent(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -58,6 +86,28 @@ RequestResult RequestHandler::BroadcastCustomEvent(const Request& request)
return RequestResult::Success();
}
/**
* Gets statistics about OBS, obs-websocket, and the current session.
*
* @responseField cpuUsage | Number | Current CPU usage in percent
* @responseField memoryUsage | Number | Amount of memory in MB currently being used by OBS
* @responseField availableDiskSpace | Number | Available disk space on the device being used for recording storage
* @responseField activeFps | Number | Current FPS being rendered
* @responseField averageFrameRenderTime | Number | Average time in milliseconds that OBS is taking to render a frame
* @responseField renderSkippedFrames | Number | Number of frames skipped by OBS in the render thread
* @responseField renderTotalFrames | Number | Total number of frames outputted by the render thread
* @responseField outputSkippedFrames | Number | Number of frames skipped by OBS in the output thread
* @responseField outputTotalFrames | Number | Total number of frames outputted by the output thread
* @responseField webSocketSessionIncomingMessages | Number | Total number of messages received by obs-websocket from the client
* @responseField webSocketSessionOutgoingMessages | Number | Total number of messages sent by obs-websocket to the client
*
* @requestType GetStats
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @category general
* @api requests
*/
RequestResult RequestHandler::GetStats(const Request&)
{
json responseData = Utils::Obs::DataHelper::GetStats();
@ -68,6 +118,18 @@ RequestResult RequestHandler::GetStats(const Request&)
return RequestResult::Success(responseData);
}
/**
* Gets an array of all hotkey names in OBS
*
* @responseField hotkeys | Array<String> | Array of hotkey names
*
* @requestType GetHotkeyList
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @category general
* @api requests
*/
RequestResult RequestHandler::GetHotkeyList(const Request&)
{
json responseData;
@ -75,6 +137,18 @@ RequestResult RequestHandler::GetHotkeyList(const Request&)
return RequestResult::Success(responseData);
}
/**
* Triggers a hotkey using its name. See `GetHotkeyList`
*
* @requestField hotkeyName | String | Name of the hotkey to trigger
*
* @requestType TriggerHotkeyByName
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @category general
* @api requests
*/
RequestResult RequestHandler::TriggerHotkeyByName(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -91,6 +165,23 @@ RequestResult RequestHandler::TriggerHotkeyByName(const Request& request)
return RequestResult::Success();
}
/**
* Triggers a hotkey using a sequence of keys.
*
* @requestField ?keyId | String | The OBS key ID to use. See https://github.com/obsproject/obs-studio/blob/master/libobs/obs-hotkeys.h | Not pressed
* @requestField ?keyModifiers | Object | Object containing key modifiers to apply | Ignored
* @requestField ?keyModifiers.shift | Boolean | Press Shift | Not pressed
* @requestField ?keyModifiers.control | Boolean | Press CTRL | Not pressed
* @requestField ?keyModifiers.alt | Boolean | Press ALT | Not pressed
* @requestField ?keyModifiers.command | Boolean | Press CMD (Mac) | Not pressed
*
* @requestType TriggerHotkeyByKeySequence
* @complexity 4
* @rpcVersion -1
* @initialVersion 5.0.0
* @category general
* @api requests
*/
RequestResult RequestHandler::TriggerHotkeyByKeySequence(const Request& request)
{
obs_key_combination_t combo = {0};
@ -125,7 +216,7 @@ RequestResult RequestHandler::TriggerHotkeyByKeySequence(const Request& request)
}
if (!combo.modifiers && (combo.key == OBS_KEY_NONE || combo.key >= OBS_KEY_LAST_VALUE))
return RequestResult::Error(RequestStatus::CannotAct, "Your provided request parameters cannot be used to trigger a hotkey.");
return RequestResult::Error(RequestStatus::CannotAct, "Your provided request fields cannot be used to trigger a hotkey.");
// Apparently things break when you don't start by setting the combo to false
obs_hotkey_inject_event(combo, false);
@ -135,6 +226,18 @@ RequestResult RequestHandler::TriggerHotkeyByKeySequence(const Request& request)
return RequestResult::Success();
}
/**
* Gets whether studio is enabled.
*
* @responseField studioModeEnabled | Boolean | Whether studio mode is enabled
*
* @requestType GetStudioModeEnabled
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category general
* @api requests
*/
RequestResult RequestHandler::GetStudioModeEnabled(const Request&)
{
json responseData;
@ -142,6 +245,18 @@ RequestResult RequestHandler::GetStudioModeEnabled(const Request&)
return RequestResult::Success(responseData);
}
/**
* Enables or disables studio mode
*
* @requestField studioModeEnabled | Boolean | True == Enabled, False == Disabled
*
* @requestType SetStudioModeEnabled
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category general
* @api requests
*/
RequestResult RequestHandler::SetStudioModeEnabled(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -163,18 +278,31 @@ RequestResult RequestHandler::SetStudioModeEnabled(const Request& request)
return RequestResult::Success();
}
/**
* Sleeps for a time duration or number of frames. Only available in request batches with types `SERIAL_REALTIME` or `SERIAL_FRAME`.
*
* @requestField sleepMillis | Number | Number of milliseconds to sleep for (if `SERIAL_REALTIME` mode) | >= 0, <= 50000
* @requestField sleepFrames | Number | Number of frames to sleep for (if `SERIAL_FRAME` mode) | >= 0, <= 10000
*
* @requestType Sleep
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @category general
* @api requests
*/
RequestResult RequestHandler::Sleep(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
if (request.RequestBatchExecutionType == OBS_WEBSOCKET_REQUEST_BATCH_EXECUTION_TYPE_SERIAL_REALTIME) {
if (request.ExecutionType == RequestBatchExecutionType::SerialRealtime) {
if (!request.ValidateNumber("sleepMillis", statusCode, comment, 0, 50000))
return RequestResult::Error(statusCode, comment);
int64_t sleepMillis = request.RequestData["sleepMillis"];
std::this_thread::sleep_for(std::chrono::milliseconds(sleepMillis));
return RequestResult::Success();
} else if (request.RequestBatchExecutionType == OBS_WEBSOCKET_REQUEST_BATCH_EXECUTION_TYPE_SERIAL_FRAME) {
} else if (request.ExecutionType == RequestBatchExecutionType::SerialFrame) {
if (!request.ValidateNumber("sleepFrames", statusCode, comment, 0, 10000))
return RequestResult::Error(statusCode, comment);
RequestResult ret = RequestResult::Success();

View File

@ -19,6 +19,20 @@ with this program. If not, see <https://www.gnu.org/licenses/>
#include "RequestHandler.h"
/**
* Gets an array of all inputs in OBS.
*
* @requestField ?inputKind | String | Restrict the array to only inputs of the specified kind | All kinds included
*
* @responseField inputs | Array<Object> | Array of inputs
*
* @requestType GetInputList
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::GetInputList(const Request& request)
{
std::string inputKind;
@ -37,6 +51,20 @@ RequestResult RequestHandler::GetInputList(const Request& request)
return RequestResult::Success(responseData);
}
/**
* Gets an array of all available input kinds in OBS.
*
* @requestField ?unversioned | Boolean | True == Return all kinds as unversioned, False == Return with version suffixes (if available) | false
*
* @responseField inputKinds | Array<String> | Array of input kinds
*
* @requestType GetInputKindList
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::GetInputKindList(const Request& request)
{
bool unversioned = false;
@ -55,6 +83,24 @@ RequestResult RequestHandler::GetInputKindList(const Request& request)
return RequestResult::Success(responseData);
}
/**
* Creates a new input, adding it as a scene item to the specified scene.
*
* @requestField sceneName | String | Name of the scene to add the input to as a scene item
* @requestField inputName | String | Name of the new input to created
* @requestField inputKind | String | The kind of input to be created
* @requestField ?inputSettings | Object | Settings object to initialize the input with | Default settings used
* @requestField ?sceneItemEnabled | Boolean | Whether to set the created scene item to enabled or disabled | True
*
* @responseField sceneItemId | Number | ID of the newly created scene item
*
* @requestType CreateInput
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::CreateInput(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -102,6 +148,20 @@ RequestResult RequestHandler::CreateInput(const Request& request)
return RequestResult::Success(responseData);
}
/**
* Removes an existing input.
*
* Note: Will immediately remove all associated scene items.
*
* @requestField inputName | String | Name of the input to remove
*
* @requestType RemoveInput
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::RemoveInput(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -118,6 +178,19 @@ RequestResult RequestHandler::RemoveInput(const Request& request)
return RequestResult::Success();
}
/**
* Sets the name of an input (rename).
*
* @requestField inputName | String | Current input name
* @requestField newInputName | String | New name for the input
*
* @requestType SetInputName
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::SetInputName(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -137,6 +210,20 @@ RequestResult RequestHandler::SetInputName(const Request& request)
return RequestResult::Success();
}
/**
* Gets the default settings for an input kind.
*
* @requestField inputKind | String | Input kind to get the default settings for
*
* @responseField defaultInputSettings | Object | Object of default settings for the input kind
*
* @requestType GetInputDefaultSettings
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::GetInputDefaultSettings(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -155,6 +242,23 @@ RequestResult RequestHandler::GetInputDefaultSettings(const Request& request)
return RequestResult::Success(responseData);
}
/**
* Gets the settings of an input.
*
* Note: Does not include defaults. To create the entire settings object, overlay `inputSettings` over the `defaultInputSettings` provided by `GetInputDefaultSettings`.
*
* @requestField inputName | String | Name of the input to get the settings of
*
* @responseField inputSettings | Object | Object of settings for the input
* @responseField inputKind | String | The kind of the input
*
* @requestType GetInputSettings
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::GetInputSettings(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -171,6 +275,19 @@ RequestResult RequestHandler::GetInputSettings(const Request& request)
return RequestResult::Success(responseData);
}
/**
* Sets the settings of an input.
*
* @requestField inputName | String | Name of the input to set the settings of
* @requestField inputSettings | Object | Object of settings to apply
*
* @requestType SetInputSettings
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::SetInputSettings(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -206,6 +323,20 @@ RequestResult RequestHandler::SetInputSettings(const Request& request)
return RequestResult::Success();
}
/**
* Gets the audio mute state of an input.
*
* @requestField inputName | String | Name of input to get the mute state of
*
* @responseField inputMuted | Boolean | Whether the input is muted
*
* @requestType GetInputMute
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::GetInputMute(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -219,6 +350,19 @@ RequestResult RequestHandler::GetInputMute(const Request& request)
return RequestResult::Success(responseData);
}
/**
* Sets the audio mute state of an input.
*
* @requestField inputName | String | Name of the input to set the mute state of
* @requestField inputMuted | Boolean | Whether to mute the input or not
*
* @requestType SetInputMute
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::SetInputMute(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -232,6 +376,20 @@ RequestResult RequestHandler::SetInputMute(const Request& request)
return RequestResult::Success();
}
/**
* Toggles the audio mute state of an input.
*
* @requestField inputName | String | Name of the input to toggle the mute state of
*
* @responseField inputMuted | Boolean | Whether the input has been muted or unmuted
*
* @requestType ToggleInputMute
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::ToggleInputMute(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -248,6 +406,21 @@ RequestResult RequestHandler::ToggleInputMute(const Request& request)
return RequestResult::Success(responseData);
}
/**
* Gets the current volume setting of an input.
*
* @requestField inputName | String | Name of the input to get the volume of
*
* @responseField inputVolumeMul | Number | Volume setting in mul
* @responseField inputVolumeDb | Number | Volume setting in dB
*
* @requestType GetInputVolume
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::GetInputVolume(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -267,6 +440,20 @@ RequestResult RequestHandler::GetInputVolume(const Request& request)
return RequestResult::Success(responseData);
}
/**
* Sets the volume setting of an input.
*
* @requestField inputName | String | Name of the input to set the volume of
* @requestField ?inputVolumeMul | Number | Volume setting in mul | >= 0, <= 20 | `inputVolumeDb` should be specified
* @requestField ?inputVolumeDb | Number | Volume setting in dB | >= -100, <= -26 | `inputVolumeMul` should be specified
*
* @requestType SetInputVolume
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::SetInputVolume(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -284,10 +471,10 @@ RequestResult RequestHandler::SetInputVolume(const Request& request)
return RequestResult::Error(statusCode, comment);
if (hasMul && hasDb)
return RequestResult::Error(RequestStatus::TooManyRequestParameters, "You may only specify one volume parameter.");
return RequestResult::Error(RequestStatus::TooManyRequestFields, "You may only specify one volume field.");
if (!hasMul && !hasDb)
return RequestResult::Error(RequestStatus::MissingRequestParameter, "You must specify one volume parameter.");
return RequestResult::Error(RequestStatus::MissingRequestField, "You must specify one volume field.");
float inputVolumeMul;
if (hasMul)
@ -300,6 +487,22 @@ RequestResult RequestHandler::SetInputVolume(const Request& request)
return RequestResult::Success();
}
/**
* Gets the audio sync offset of an input.
*
* Note: The audio sync offset can be negative too!
*
* @requestField inputName | String | Name of the input to get the audio sync offset of
*
* @responseField inputAudioSyncOffset | Number | Audio sync offset in milliseconds
*
* @requestType GetInputAudioSyncOffset
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::GetInputAudioSyncOffset(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -315,6 +518,19 @@ RequestResult RequestHandler::GetInputAudioSyncOffset(const Request& request)
return RequestResult::Success(responseData);
}
/**
* Sets the audio sync offset of an input.
*
* @requestField inputName | String | Name of the input to set the audio sync offset of
* @requestField inputAudioSyncOffset | Number | New audio sync offset in milliseconds | >= -950, <= 20000
*
* @requestType SetInputAudioSyncOffset
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::SetInputAudioSyncOffset(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -329,6 +545,25 @@ RequestResult RequestHandler::SetInputAudioSyncOffset(const Request& request)
return RequestResult::Success();
}
/**
* Gets the audio monitor type of an input.
*
* The available audio monitor types are:
* - `OBS_MONITORING_TYPE_NONE`
* - `OBS_MONITORING_TYPE_MONITOR_ONLY`
* - `OBS_MONITORING_TYPE_MONITOR_AND_OUTPUT`
*
* @requestField inputName | String | Name of the input to get the audio monitor type of
*
* @responseField monitorType | String | Audio monitor type
*
* @requestType GetInputAudioMonitorType
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::GetInputAudioMonitorType(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -343,6 +578,19 @@ RequestResult RequestHandler::GetInputAudioMonitorType(const Request& request)
return RequestResult::Success(responseData);
}
/**
* Sets the audio monitor type of an input.
*
* @requestField inputName | String | Name of the input to set the audio monitor type of
* @requestField monitorType | String | Audio monitor type
*
* @requestType SetInputAudioMonitorType
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::SetInputAudioMonitorType(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -360,7 +608,7 @@ RequestResult RequestHandler::SetInputAudioMonitorType(const Request& request)
else if (monitorTypeString == "OBS_MONITORING_TYPE_MONITOR_AND_OUTPUT")
monitorType = OBS_MONITORING_TYPE_MONITOR_AND_OUTPUT;
else
return RequestResult::Error(RequestStatus::InvalidRequestParameter, std::string("Unknown monitor type: ") + monitorTypeString);
return RequestResult::Error(RequestStatus::InvalidRequestField, std::string("Unknown monitor type: ") + monitorTypeString);
obs_source_set_monitoring_type(input, monitorType);
@ -393,6 +641,23 @@ std::vector<json> GetListPropertyItems(obs_property_t *property)
return ret;
}
/**
* Gets the items of a list property from an input's properties.
*
* Note: Use this in cases where an input provides a dynamic, selectable list of items. For example, display capture, where it provides a list of available displays.
*
* @requestField inputName | String | Name of the input
* @requestField propertyName | String | Name of the list property to get the items of
*
* @responseField propertyItems | Array<Object> | Array of items in the list property
*
* @requestType GetInputPropertiesListPropertyItems
* @complexity 4
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::GetInputPropertiesListPropertyItems(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -416,6 +681,21 @@ RequestResult RequestHandler::GetInputPropertiesListPropertyItems(const Request&
return RequestResult::Success(responseData);
}
/**
* Presses a button in the properties of an input.
*
* Note: Use this in cases where there is a button in the properties of an input that cannot be accessed in any other way. For example, browser sources, where there is a refresh button.
*
* @requestField inputName | String | Name of the input
* @requestField propertyName | String | Name of the button property to press
*
* @requestType PressInputPropertiesButton
* @complexity 4
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::PressInputPropertiesButton(const Request& request)
{
RequestStatus::RequestStatus statusCode;

View File

@ -102,7 +102,7 @@ RequestResult RequestHandler::TriggerMediaInputAction(const Request& request)
switch (mediaAction) {
default:
case OBS_WEBSOCKET_MEDIA_INPUT_ACTION_NONE:
return RequestResult::Error(RequestStatus::InvalidRequestParameter, "You have specified an invalid media input action.");
return RequestResult::Error(RequestStatus::InvalidRequestField, "You have specified an invalid media input action.");
case OBS_WEBSOCKET_MEDIA_INPUT_ACTION_PLAY:
// Shoutout to whoever implemented this API call like this
obs_source_media_play_pause(input, false);

View File

@ -227,7 +227,7 @@ RequestResult RequestHandler::SetSceneItemTransform(const Request& request)
float scaleX = r.RequestData["scaleX"];
float finalWidth = scaleX * sourceWidth;
if (!(finalWidth > -90001.0 && finalWidth < 90001.0))
return RequestResult::Error(RequestStatus::RequestParameterOutOfRange, "The parameter scaleX is too small or large for the current source resolution.");
return RequestResult::Error(RequestStatus::RequestFieldOutOfRange, "The field scaleX is too small or large for the current source resolution.");
sceneItemTransform.scale.x = scaleX;
transformChanged = true;
}
@ -237,7 +237,7 @@ RequestResult RequestHandler::SetSceneItemTransform(const Request& request)
float scaleY = r.RequestData["scaleY"];
float finalHeight = scaleY * sourceHeight;
if (!(finalHeight > -90001.0 && finalHeight < 90001.0))
return RequestResult::Error(RequestStatus::RequestParameterOutOfRange, "The parameter scaleY is too small or large for the current source resolution.");
return RequestResult::Error(RequestStatus::RequestFieldOutOfRange, "The field scaleY is too small or large for the current source resolution.");
sceneItemTransform.scale.y = scaleY;
transformChanged = true;
}
@ -255,7 +255,7 @@ RequestResult RequestHandler::SetSceneItemTransform(const Request& request)
std::string boundsTypeString = r.RequestData["boundsType"];
enum obs_bounds_type boundsType = Utils::Obs::EnumHelper::GetSceneItemBoundsType(boundsTypeString);
if (boundsType == OBS_BOUNDS_NONE && boundsTypeString != "OBS_BOUNDS_NONE")
return RequestResult::Error(RequestStatus::InvalidRequestParameter, "The parameter boundsType has an invalid value.");
return RequestResult::Error(RequestStatus::InvalidRequestField, "The field boundsType has an invalid value.");
sceneItemTransform.bounds_type = boundsType;
transformChanged = true;
}

View File

@ -19,6 +19,20 @@ with this program. If not, see <https://www.gnu.org/licenses/>
#include "RequestHandler.h"
/**
* Gets an array of all scenes in OBS.
*
* @responseField scenes | Array<String> | Array of scenes in OBS
* @responseField currentProgramSceneName | String | Current program scene
* @responseField currentPreviewSceneName | String | Current preview scene. `null` if not in studio mode
*
* @requestType GetSceneList
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category sources
*/
RequestResult RequestHandler::GetSceneList(const Request&)
{
json responseData;
@ -40,6 +54,18 @@ RequestResult RequestHandler::GetSceneList(const Request&)
return RequestResult::Success(responseData);
}
/**
* Gets the current program scene.
*
* @responseField currentProgramSceneName | String | Current program scene
*
* @requestType GetCurrentProgramScene
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category sources
*/
RequestResult RequestHandler::GetCurrentProgramScene(const Request&)
{
json responseData;
@ -49,6 +75,18 @@ RequestResult RequestHandler::GetCurrentProgramScene(const Request&)
return RequestResult::Success(responseData);
}
/**
* Sets the current program scene.
*
* @requestField sceneName | String | Scene to set as the current program scene
*
* @requestType SetCurrentProgramScene
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category sources
*/
RequestResult RequestHandler::SetCurrentProgramScene(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -62,6 +100,20 @@ RequestResult RequestHandler::SetCurrentProgramScene(const Request& request)
return RequestResult::Success();
}
/**
* Gets the current preview scene.
*
* Only available when studio mode is enabled.
*
* @responseField currentPreviewSceneName | String | Current preview scene
*
* @requestType GetCurrentPreviewScene
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category sources
*/
RequestResult RequestHandler::GetCurrentPreviewScene(const Request&)
{
if (!obs_frontend_preview_program_mode_active())
@ -75,6 +127,20 @@ RequestResult RequestHandler::GetCurrentPreviewScene(const Request&)
return RequestResult::Success(responseData);
}
/**
* Sets the current preview scene.
*
* Only available when studio mode is enabled.
*
* @requestField sceneName | String | Scene to set as the current preview scene
*
* @requestType SetCurrentPreviewScene
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category sources
*/
RequestResult RequestHandler::SetCurrentPreviewScene(const Request& request)
{
if (!obs_frontend_preview_program_mode_active())
@ -91,6 +157,18 @@ RequestResult RequestHandler::SetCurrentPreviewScene(const Request& request)
return RequestResult::Success();
}
/**
* Creates a new scene in OBS.
*
* @requestField sceneName | String | Name for the new scene
*
* @requestType CreateScene
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category sources
*/
RequestResult RequestHandler::CreateScene(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -113,6 +191,18 @@ RequestResult RequestHandler::CreateScene(const Request& request)
return RequestResult::Success();
}
/**
* Removes a scene from OBS.
*
* @requestField sceneName | String | Name of the scene to remove
*
* @requestType RemoveScene
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category sources
*/
RequestResult RequestHandler::RemoveScene(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -129,6 +219,19 @@ RequestResult RequestHandler::RemoveScene(const Request& request)
return RequestResult::Success();
}
/**
* Sets the name of a scene (rename).
*
* @requestField sceneName | String | Name of the scene to be renamed
* @requestField newSceneName | String | New name for the scene
*
* @requestType SetSceneName
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category sources
*/
RequestResult RequestHandler::SetSceneName(const Request& request)
{
RequestStatus::RequestStatus statusCode;

View File

@ -109,6 +109,23 @@ bool IsImageFormatValid(std::string format)
return supportedFormats.contains(format.c_str());
}
/**
* Gets the active and show state of a source.
*
* **Compatible with inputs and scenes.**
*
* @requestField sourceName | String | Name of the source to get the active state of
*
* @responseField videoActive | Boolean | Whether the source is showing in Program
* @responseField videoShowing | Boolean | Whether the source is showing in the UI (Preview, Projector, Properties)
*
* @requestType GetSourceActive
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category sources
*/
RequestResult RequestHandler::GetSourceActive(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -126,6 +143,29 @@ RequestResult RequestHandler::GetSourceActive(const Request& request)
return RequestResult::Success(responseData);
}
/**
* Gets a Base64-encoded screenshot of a source.
*
* The `imageWidth` and `imageHeight` parameters are treated as "scale to inner", meaning the smallest ratio will be used and the aspect ratio of the original resolution is kept.
* If `imageWidth` and `imageHeight` are not specified, the compressed image will use the full resolution of the source.
*
* **Compatible with inputs and scenes.**
*
* @requestField sourceName | String | Name of the source to take a screenshot of
* @requestField imageFormat | String | Image compression format to use. Use `GetVersion` to get compatible image formats
* @requestField ?imageWidth | Number | Width to scale the screenshot to | >= 8, <= 4096 | Source value is used
* @requestField ?imageHeight | Number | Height to scale the screenshot to | >= 8, <= 4096 | Source value is used
* @requestField ?imageCompressionQuality | Number | Compression quality to use. 0 for high compression, 100 for uncompressed. -1 to use "default" (whatever that means, idk) | >= -1, <= 100 | -1
*
* @responseField imageData | String | Base64-encoded screenshot
*
* @requestType GetSourceScreenshot
* @complexity 4
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category sources
*/
RequestResult RequestHandler::GetSourceScreenshot(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -140,7 +180,7 @@ RequestResult RequestHandler::GetSourceScreenshot(const Request& request)
std::string imageFormat = request.RequestData["imageFormat"];
if (!IsImageFormatValid(imageFormat))
return RequestResult::Error(RequestStatus::InvalidRequestParameter, "Your specified image format is invalid or not supported by this system.");
return RequestResult::Error(RequestStatus::InvalidRequestField, "Your specified image format is invalid or not supported by this system.");
uint32_t requestedWidth{0};
uint32_t requestedHeight{0};
@ -189,6 +229,30 @@ RequestResult RequestHandler::GetSourceScreenshot(const Request& request)
return RequestResult::Success(responseData);
}
/**
* Saves a screenshot of a source to the filesystem.
*
* The `imageWidth` and `imageHeight` parameters are treated as "scale to inner", meaning the smallest ratio will be used and the aspect ratio of the original resolution is kept.
* If `imageWidth` and `imageHeight` are not specified, the compressed image will use the full resolution of the source.
*
* **Compatible with inputs and scenes.**
*
* @requestField sourceName | String | Name of the source to take a screenshot of
* @requestField imageFormat | String | Image compression format to use. Use `GetVersion` to get compatible image formats
* @requestField imageFilePath | String | Path to save the screenshot file to. Eg. `C:\Users\user\Desktop\screenshot.png`
* @requestField ?imageWidth | Number | Width to scale the screenshot to | >= 8, <= 4096 | Source value is used
* @requestField ?imageHeight | Number | Height to scale the screenshot to | >= 8, <= 4096 | Source value is used
* @requestField ?imageCompressionQuality | Number | Compression quality to use. 0 for high compression, 100 for uncompressed. -1 to use "default" (whatever that means, idk) | >= -1, <= 100 | -1
*
* @responseField imageData | String | Base64-encoded screenshot
*
* @requestType GetSourceScreenshot
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category sources
*/
RequestResult RequestHandler::SaveSourceScreenshot(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -204,7 +268,7 @@ RequestResult RequestHandler::SaveSourceScreenshot(const Request& request)
std::string imageFilePath = request.RequestData["imageFilePath"];
if (!IsImageFormatValid(imageFormat))
return RequestResult::Error(RequestStatus::InvalidRequestParameter, "Your specified image format is invalid or not supported by this system.");
return RequestResult::Error(RequestStatus::InvalidRequestField, "Your specified image format is invalid or not supported by this system.");
QFileInfo filePathInfo(QString::fromStdString(imageFilePath));
if (!filePathInfo.absoluteDir().exists())

View File

@ -34,15 +34,15 @@ Request::Request(const std::string &requestType, const json &requestData) :
RequestType(requestType),
HasRequestData(requestData.is_object()),
RequestData(GetDefaultJsonObject(requestData)),
RequestBatchExecutionType(OBS_WEBSOCKET_REQUEST_BATCH_EXECUTION_TYPE_NONE)
ExecutionType(RequestBatchExecutionType::None)
{
}
Request::Request(const std::string &requestType, const json &requestData, const ObsWebSocketRequestBatchExecutionType requestBatchExecutionType) :
Request::Request(const std::string &requestType, const json &requestData, RequestBatchExecutionType::RequestBatchExecutionType executionType) :
RequestType(requestType),
HasRequestData(requestData.is_object()),
RequestData(GetDefaultJsonObject(requestData)),
RequestBatchExecutionType(requestBatchExecutionType)
ExecutionType(executionType)
{
}
@ -60,8 +60,8 @@ bool Request::ValidateBasic(const std::string &keyName, RequestStatus::RequestSt
}
if (!RequestData.contains(keyName) || RequestData[keyName].is_null()) {
statusCode = RequestStatus::MissingRequestParameter;
comment = std::string("Your request is missing the `") + keyName + "` parameter.";
statusCode = RequestStatus::MissingRequestField;
comment = std::string("Your request is missing the `") + keyName + "` field.";
return false;
}
@ -71,20 +71,20 @@ bool Request::ValidateBasic(const std::string &keyName, RequestStatus::RequestSt
bool Request::ValidateOptionalNumber(const std::string &keyName, RequestStatus::RequestStatus &statusCode, std::string &comment, const double minValue, const double maxValue) const
{
if (!RequestData[keyName].is_number()) {
statusCode = RequestStatus::InvalidRequestParameterType;
comment = std::string("The parameter `") + keyName + "` must be a number.";
statusCode = RequestStatus::InvalidRequestFieldType;
comment = std::string("The field value of `") + keyName + "` must be a number.";
return false;
}
double value = RequestData[keyName];
if (value < minValue) {
statusCode = RequestStatus::RequestParameterOutOfRange;
comment = std::string("The parameter `") + keyName + "` is below the minimum of `" + std::to_string(minValue) + "`";
statusCode = RequestStatus::RequestFieldOutOfRange;
comment = std::string("The field value of `") + keyName + "` is below the minimum of `" + std::to_string(minValue) + "`";
return false;
}
if (value > maxValue) {
statusCode = RequestStatus::RequestParameterOutOfRange;
comment = std::string("The parameter `") + keyName + "` is above the maximum of `" + std::to_string(maxValue) + "`";
statusCode = RequestStatus::RequestFieldOutOfRange;
comment = std::string("The field value of `") + keyName + "` is above the maximum of `" + std::to_string(maxValue) + "`";
return false;
}
@ -105,14 +105,14 @@ bool Request::ValidateNumber(const std::string &keyName, RequestStatus::RequestS
bool Request::ValidateOptionalString(const std::string &keyName, RequestStatus::RequestStatus &statusCode, std::string &comment, const bool allowEmpty) const
{
if (!RequestData[keyName].is_string()) {
statusCode = RequestStatus::InvalidRequestParameterType;
comment = std::string("The parameter `") + keyName + "` must be a string.";
statusCode = RequestStatus::InvalidRequestFieldType;
comment = std::string("The field value of `") + keyName + "` must be a string.";
return false;
}
if (RequestData[keyName].get<std::string>().empty() && !allowEmpty) {
statusCode = RequestStatus::RequestParameterEmpty;
comment = std::string("The parameter `") + keyName + "` must not be empty.";
statusCode = RequestStatus::RequestFieldEmpty;
comment = std::string("The field value of `") + keyName + "` must not be empty.";
return false;
}
@ -133,8 +133,8 @@ bool Request::ValidateString(const std::string &keyName, RequestStatus::RequestS
bool Request::ValidateOptionalBoolean(const std::string &keyName, RequestStatus::RequestStatus &statusCode, std::string &comment) const
{
if (!RequestData[keyName].is_boolean()) {
statusCode = RequestStatus::InvalidRequestParameterType;
comment = std::string("The parameter `") + keyName + "` must be boolean.";
statusCode = RequestStatus::InvalidRequestFieldType;
comment = std::string("The field value of `") + keyName + "` must be boolean.";
return false;
}
@ -155,14 +155,14 @@ bool Request::ValidateBoolean(const std::string &keyName, RequestStatus::Request
bool Request::ValidateOptionalObject(const std::string &keyName, RequestStatus::RequestStatus &statusCode, std::string &comment, const bool allowEmpty) const
{
if (!RequestData[keyName].is_object()) {
statusCode = RequestStatus::InvalidRequestParameterType;
comment = std::string("The parameter `") + keyName + "` must be an object.";
statusCode = RequestStatus::InvalidRequestFieldType;
comment = std::string("The field value of `") + keyName + "` must be an object.";
return false;
}
if (RequestData[keyName].empty() && !allowEmpty) {
statusCode = RequestStatus::RequestParameterEmpty;
comment = std::string("The parameter `") + keyName + "` must not be empty.";
statusCode = RequestStatus::RequestFieldEmpty;
comment = std::string("The field value of `") + keyName + "` must not be empty.";
return false;
}
@ -183,14 +183,14 @@ bool Request::ValidateObject(const std::string &keyName, RequestStatus::RequestS
bool Request::ValidateOptionalArray(const std::string &keyName, RequestStatus::RequestStatus &statusCode, std::string &comment, const bool allowEmpty) const
{
if (!RequestData[keyName].is_array()) {
statusCode = RequestStatus::InvalidRequestParameterType;
comment = std::string("The parameter `") + keyName + "` must be an array.";
statusCode = RequestStatus::InvalidRequestFieldType;
comment = std::string("The field value of `") + keyName + "` must be an array.";
return false;
}
if (RequestData[keyName].empty() && !allowEmpty) {
statusCode = RequestStatus::RequestParameterEmpty;
comment = std::string("The parameter `") + keyName + "` must not be empty.";
statusCode = RequestStatus::RequestFieldEmpty;
comment = std::string("The field value of `") + keyName + "` must not be empty.";
return false;
}

View File

@ -20,15 +20,9 @@ with this program. If not, see <https://www.gnu.org/licenses/>
#pragma once
#include "../types/RequestStatus.h"
#include "../types/RequestBatchExecutionType.h"
#include "../../utils/Json.h"
enum ObsWebSocketRequestBatchExecutionType {
OBS_WEBSOCKET_REQUEST_BATCH_EXECUTION_TYPE_NONE,
OBS_WEBSOCKET_REQUEST_BATCH_EXECUTION_TYPE_SERIAL_REALTIME,
OBS_WEBSOCKET_REQUEST_BATCH_EXECUTION_TYPE_SERIAL_FRAME,
OBS_WEBSOCKET_REQUEST_BATCH_EXECUTION_TYPE_PARALLEL
};
enum ObsWebSocketSceneFilter {
OBS_WEBSOCKET_SCENE_FILTER_SCENE_ONLY,
OBS_WEBSOCKET_SCENE_FILTER_GROUP_ONLY,
@ -38,7 +32,7 @@ enum ObsWebSocketSceneFilter {
struct Request
{
Request(const std::string &requestType, const json &requestData = nullptr);
Request(const std::string &requestType, const json &requestData, const ObsWebSocketRequestBatchExecutionType requestBatchExecutionType);
Request(const std::string &requestType, const json &requestData, RequestBatchExecutionType::RequestBatchExecutionType executionType);
// Contains the key and is not null
bool Contains(const std::string &keyName) const;
@ -65,5 +59,5 @@ struct Request
std::string RequestType;
bool HasRequestData;
json RequestData;
ObsWebSocketRequestBatchExecutionType RequestBatchExecutionType;
RequestBatchExecutionType::RequestBatchExecutionType ExecutionType;
};

View File

@ -0,0 +1,84 @@
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#pragma once
#include <stdint.h>
namespace RequestBatchExecutionType {
enum RequestBatchExecutionType {
/**
* Not a request batch.
*
* @enumIdentifier None
* @enumValue 0
* @enumType RequestBatchExecutionType
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
None = 0,
/**
* A request batch which processes all requests serially, as fast as possible.
*
* Note: To introduce artificial delay, use the `Sleep` request and the `sleepMillis` request field.
*
* @enumIdentifier SerialRealtime
* @enumValue 1
* @enumType RequestBatchExecutionType
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
SerialRealtime = 1,
/**
* A request batch type which processes all requests serially, in sync with the graphics thread. Designed to
* provide high accuracy for animations.
*
* Note: To introduce artificial delay, use the `Sleep` request and the `sleepFrames` request field.
*
* @enumIdentifier SerialFrame
* @enumValue 2
* @enumType RequestBatchExecutionType
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
SerialFrame = 2,
/**
* A request batch type which processes all requests using all available threads in the thread pool.
*
* Note: This is mainly experimental, and only really shows its colors during requests which require lots of
* active processing, like `GetSourceScreenshot`.
*
* @enumIdentifier Parallel
* @enumValue 3
* @enumType RequestBatchExecutionType
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
Parallel = 3,
};
inline bool IsValid(uint8_t executionType)
{
return executionType >= None && executionType <= Parallel;
}
}

View File

@ -21,73 +21,363 @@ with this program. If not, see <https://www.gnu.org/licenses/>
namespace RequestStatus {
enum RequestStatus {
/**
* Unknown status, should never be used.
*
* @enumIdentifier Unknown
* @enumValue 0
* @enumType RequestStatus
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
Unknown = 0,
// For internal use to signify a successful parameter check
/**
* For internal use to signify a successful field check.
*
* @enumIdentifier NoError
* @enumValue 10
* @enumType RequestStatus
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
NoError = 10,
/**
* The request has succeeded.
*
* @enumIdentifier Success
* @enumValue 100
* @enumType RequestStatus
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
Success = 100,
// The `requestType` field is missing from the request data
/**
* The `requestType` field is missing from the request data.
*
* @enumIdentifier MissingRequestType
* @enumValue 203
* @enumType RequestStatus
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
MissingRequestType = 203,
// The request type is invalid or does not exist
/**
* The request type is invalid or does not exist.
*
* @enumIdentifier UnknownRequestType
* @enumValue 204
* @enumType RequestStatus
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
UnknownRequestType = 204,
// Generic error code (comment required)
/**
* Generic error code.
*
* Note: A comment is required to be provided by obs-websocket.
*
* @enumIdentifier GenericError
* @enumValue 205
* @enumType RequestStatus
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
GenericError = 205,
// The request batch execution type is not supported
/**
* The request batch execution type is not supported.
*
* @enumIdentifier UnsupportedRequestBatchExecutionType
* @enumValue 206
* @enumType RequestStatus
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
UnsupportedRequestBatchExecutionType = 206,
// A required request parameter is missing
MissingRequestParameter = 300,
// The request does not have a valid requestData object.
/**
* A required request field is missing.
*
* @enumIdentifier MissingRequestField
* @enumValue 300
* @enumType RequestStatus
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
MissingRequestField = 300,
/**
* The request does not have a valid requestData object.
*
* @enumIdentifier MissingRequestData
* @enumValue 301
* @enumType RequestStatus
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
MissingRequestData = 301,
// Generic invalid request parameter message (comment required)
InvalidRequestParameter = 400,
// A request parameter has the wrong data type
InvalidRequestParameterType = 401,
// A request parameter (float or int) is out of valid range
RequestParameterOutOfRange = 402,
// A request parameter (string or array) is empty and cannot be
RequestParameterEmpty = 403,
// There are too many request parameters (eg. a request takes two optionals, where only one is allowed at a time)
TooManyRequestParameters = 404,
/**
* Generic invalid request field message.
*
* Note: A comment is required to be provided by obs-websocket.
*
* @enumIdentifier InvalidRequestField
* @enumValue 400
* @enumType RequestStatus
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
InvalidRequestField = 400,
/**
* A request field has the wrong data type.
*
* @enumIdentifier InvalidRequestFieldType
* @enumValue 401
* @enumType RequestStatus
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
InvalidRequestFieldType = 401,
/**
* A request field (number) is outside of the allowed range.
*
* @enumIdentifier RequestFieldOutOfRange
* @enumValue 402
* @enumType RequestStatus
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
RequestFieldOutOfRange = 402,
/**
* A request field (string or array) is empty and cannot be.
*
* @enumIdentifier RequestFieldEmpty
* @enumValue 403
* @enumType RequestStatus
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
RequestFieldEmpty = 403,
/**
* There are too many request fields (eg. a request takes two optionals, where only one is allowed at a time).
*
* @enumIdentifier TooManyRequestFields
* @enumValue 404
* @enumType RequestStatus
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
TooManyRequestFields = 404,
// An output is running and cannot be in order to perform the request (generic)
/**
* An output is running and cannot be in order to perform the request.
*
* @enumIdentifier OutputRunning
* @enumValue 500
* @enumType RequestStatus
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
OutputRunning = 500,
// An output is not running and should be
/**
* An output is not running and should be.
*
* @enumIdentifier OutputNotRunning
* @enumValue 501
* @enumType RequestStatus
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
OutputNotRunning = 501,
// An output is paused and should not be
/**
* An output is paused and should not be.
*
* @enumIdentifier OutputPaused
* @enumValue 502
* @enumType RequestStatus
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
OutputPaused = 502,
// An output is disabled and should not be
OutputDisabled = 503,
// Studio mode is active and cannot be
StudioModeActive = 504,
// Studio mode is not active and should be
StudioModeNotActive = 505,
// An output is not paused and should be
OutputNotPaused = 506,
/**
* An output is not paused and should be.
*
* @enumIdentifier OutputNotPaused
* @enumValue 503
* @enumType RequestStatus
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
OutputNotPaused = 503,
/**
* An output is disabled and should not be.
*
* @enumIdentifier OutputDisabled
* @enumValue 504
* @enumType RequestStatus
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
OutputDisabled = 504,
/**
* Studio mode is active and cannot be.
*
* @enumIdentifier StudioModeActive
* @enumValue 505
* @enumType RequestStatus
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
StudioModeActive = 505,
/**
* Studio mode is not active and should be.
*
* @enumIdentifier StudioModeNotActive
* @enumValue 506
* @enumType RequestStatus
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
StudioModeNotActive = 506,
// The resource was not found
/**
* The resource was not found.
*
* Note: Resources are any kind of object in obs-websocket, like inputs, profiles, outputs, etc.
*
* @enumIdentifier ResourceNotFound
* @enumValue 600
* @enumType RequestStatus
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
ResourceNotFound = 600,
// The resource already exists
/**
* The resource already exists.
*
* @enumIdentifier ResourceAlreadyExists
* @enumValue 601
* @enumType RequestStatus
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
ResourceAlreadyExists = 601,
// The type of resource found is invalid
/**
* The type of resource found is invalid.
*
* @enumIdentifier InvalidResourceType
* @enumValue 602
* @enumType RequestStatus
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
InvalidResourceType = 602,
// There are not enough instances of the resource in order to perform the request
/**
* There are not enough instances of the resource in order to perform the request.
*
* @enumIdentifier NotEnoughResources
* @enumValue 603
* @enumType RequestStatus
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
NotEnoughResources = 603,
// The state of the resource is invalid. For example, if the resource is blocked from being accessed
/**
* The state of the resource is invalid. For example, if the resource is blocked from being accessed.
*
* @enumIdentifier InvalidResourceState
* @enumValue 604
* @enumType RequestStatus
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
InvalidResourceState = 604,
// The specified input (obs_source_t-OBS_SOURCE_TYPE_INPUT) had the wrong kind
/**
* The specified input (obs_source_t-OBS_SOURCE_TYPE_INPUT) had the wrong kind.
*
* @enumIdentifier InvalidInputKind
* @enumValue 605
* @enumType RequestStatus
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
InvalidInputKind = 605,
// Creating the resource failed
/**
* Creating the resource failed.
*
* @enumIdentifier ResourceCreationFailed
* @enumValue 700
* @enumType RequestStatus
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
ResourceCreationFailed = 700,
// Performing an action on the resource failed
/**
* Performing an action on the resource failed.
*
* @enumIdentifier ResourceActionFailed
* @enumValue 701
* @enumType RequestStatus
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
ResourceActionFailed = 701,
// Processing the request failed unexpectedly (comment required)
/**
* Processing the request failed unexpectedly.
*
* Note: A comment is required to be provided by obs-websocket.
*
* @enumIdentifier RequestProcessingFailed
* @enumValue 702
* @enumType RequestStatus
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
RequestProcessingFailed = 702,
// The combination of request parameters cannot be used to perform an action
/**
* The combination of request fields cannot be used to perform an action.
*
* @enumIdentifier CannotAct
* @enumValue 703
* @enumType RequestStatus
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
CannotAct = 703,
};
}

View File

@ -92,7 +92,7 @@ class WebSocketServer : QObject
void SetSessionParameters(SessionPtr session, WebSocketServer::ProcessResult &ret, const json &payloadData);
void ProcessMessage(SessionPtr session, ProcessResult &ret, WebSocketOpCode::WebSocketOpCode opCode, const json &payloadData);
void ProcessRequestBatch(SessionPtr session, ObsWebSocketRequestBatchExecutionType executionType, const std::vector<json> &requests, std::vector<json> &results, json &variables);
void ProcessRequestBatch(SessionPtr session, RequestBatchExecutionType::RequestBatchExecutionType executionType, const std::vector<json> &requests, std::vector<json> &results, json &variables);
QThreadPool _threadPool;

View File

@ -37,7 +37,7 @@ void WebSocketServer::SetSessionParameters(SessionPtr session, ProcessResult &re
{
if (payloadData.contains("ignoreInvalidMessages")) {
if (!payloadData["ignoreInvalidMessages"].is_boolean()) {
ret.closeCode = WebSocketCloseCode::InvalidDataKeyType;
ret.closeCode = WebSocketCloseCode::InvalidDataFieldType;
ret.closeReason = "Your `ignoreInvalidMessages` is not a boolean.";
return;
}
@ -46,7 +46,7 @@ void WebSocketServer::SetSessionParameters(SessionPtr session, ProcessResult &re
if (payloadData.contains("eventSubscriptions")) {
if (!payloadData["eventSubscriptions"].is_number_unsigned()) {
ret.closeCode = WebSocketCloseCode::InvalidDataKeyType;
ret.closeCode = WebSocketCloseCode::InvalidDataFieldType;
ret.closeReason = "Your `eventSubscriptions` is not an unsigned number.";
return;
}
@ -58,10 +58,10 @@ void WebSocketServer::ProcessMessage(SessionPtr session, WebSocketServer::Proces
{
if (!payloadData.is_object()) {
if (payloadData.is_null()) {
ret.closeCode = WebSocketCloseCode::MissingDataKey;
ret.closeCode = WebSocketCloseCode::MissingDataField;
ret.closeReason = "Your payload is missing data (`d`).";
} else {
ret.closeCode = WebSocketCloseCode::InvalidDataKeyType;
ret.closeCode = WebSocketCloseCode::InvalidDataFieldType;
ret.closeReason = "Your payload's data (`d`) is not an object.";
}
return;
@ -105,13 +105,13 @@ void WebSocketServer::ProcessMessage(SessionPtr session, WebSocketServer::Proces
}
if (!payloadData.contains("rpcVersion")) {
ret.closeCode = WebSocketCloseCode::MissingDataKey;
ret.closeCode = WebSocketCloseCode::MissingDataField;
ret.closeReason = "Your payload's data is missing an `rpcVersion`.";
return;
}
if (!payloadData["rpcVersion"].is_number_unsigned()) {
ret.closeCode = WebSocketCloseCode::InvalidDataKeyType;
ret.closeCode = WebSocketCloseCode::InvalidDataFieldType;
ret.closeReason = "Your `rpcVersion` is not an unsigned number.";
}
@ -168,7 +168,7 @@ void WebSocketServer::ProcessMessage(SessionPtr session, WebSocketServer::Proces
// RequestID checking has to be done here where we are able to close the connection.
if (!payloadData.contains("requestId")) {
if (!session->IgnoreInvalidMessages()) {
ret.closeCode = WebSocketCloseCode::MissingDataKey;
ret.closeCode = WebSocketCloseCode::MissingDataField;
ret.closeReason = "Your payload data is missing a `requestId`.";
}
return;
@ -197,7 +197,7 @@ void WebSocketServer::ProcessMessage(SessionPtr session, WebSocketServer::Proces
// RequestID checking has to be done here where we are able to close the connection.
if (!payloadData.contains("requestId")) {
if (!session->IgnoreInvalidMessages()) {
ret.closeCode = WebSocketCloseCode::MissingDataKey;
ret.closeCode = WebSocketCloseCode::MissingDataField;
ret.closeReason = "Your payload data is missing a `requestId`.";
}
return;
@ -205,7 +205,7 @@ void WebSocketServer::ProcessMessage(SessionPtr session, WebSocketServer::Proces
if (!payloadData.contains("requests")) {
if (!session->IgnoreInvalidMessages()) {
ret.closeCode = WebSocketCloseCode::MissingDataKey;
ret.closeCode = WebSocketCloseCode::MissingDataField;
ret.closeReason = "Your payload data is missing a `requests`.";
}
return;
@ -213,40 +213,35 @@ void WebSocketServer::ProcessMessage(SessionPtr session, WebSocketServer::Proces
if (!payloadData["requests"].is_array()) {
if (!session->IgnoreInvalidMessages()) {
ret.closeCode = WebSocketCloseCode::InvalidDataKeyType;
ret.closeCode = WebSocketCloseCode::InvalidDataFieldType;
ret.closeReason = "Your `requests` is not an array.";
}
return;
}
ObsWebSocketRequestBatchExecutionType executionType = OBS_WEBSOCKET_REQUEST_BATCH_EXECUTION_TYPE_SERIAL_REALTIME;
RequestBatchExecutionType::RequestBatchExecutionType executionType = RequestBatchExecutionType::SerialRealtime;
if (payloadData.contains("executionType") && !payloadData["executionType"].is_null()) {
if (!payloadData["executionType"].is_string()) {
if (!payloadData["executionType"].is_number_unsigned()) {
if (!session->IgnoreInvalidMessages()) {
ret.closeCode = WebSocketCloseCode::InvalidDataKeyType;
ret.closeReason = "Your `executionType` is not a string.";
ret.closeCode = WebSocketCloseCode::InvalidDataFieldType;
ret.closeReason = "Your `executionType` is not a number.";
}
return;
}
std::string executionTypeString = payloadData["executionType"];
if (executionTypeString == "OBS_WEBSOCKET_REQUEST_BATCH_EXECUTION_TYPE_SERIAL_REALTIME") {
executionType = OBS_WEBSOCKET_REQUEST_BATCH_EXECUTION_TYPE_SERIAL_REALTIME;
} else if (executionTypeString == "OBS_WEBSOCKET_REQUEST_BATCH_EXECUTION_TYPE_SERIAL_FRAME") {
executionType = OBS_WEBSOCKET_REQUEST_BATCH_EXECUTION_TYPE_SERIAL_FRAME;
} else if (executionTypeString == "OBS_WEBSOCKET_REQUEST_BATCH_EXECUTION_TYPE_PARALLEL") {
if (_threadPool.maxThreadCount() < 2) {
if (!session->IgnoreInvalidMessages()) {
ret.closeCode = WebSocketCloseCode::UnsupportedFeature;
ret.closeReason = "Parallel request batch processing is not available on this system due to limited core count.";
}
return;
}
executionType = OBS_WEBSOCKET_REQUEST_BATCH_EXECUTION_TYPE_PARALLEL;
} else {
uint8_t executionType = payloadData["executionType"];
if (!RequestBatchExecutionType::IsValid(executionType) || executionType == RequestBatchExecutionType::None) {
if (!session->IgnoreInvalidMessages()) {
ret.closeCode = WebSocketCloseCode::InvalidDataKeyValue;
ret.closeReason = "Your `executionType`'s value is not recognized.";
ret.closeCode = WebSocketCloseCode::InvalidDataFieldValue;
ret.closeReason = "Your `executionType` has an invalid value.";
}
}
// The thread pool must support 2 or more threads else parallel requests will deadlock.
if (executionType == RequestBatchExecutionType::Parallel && _threadPool.maxThreadCount() < 2) {
if (!session->IgnoreInvalidMessages()) {
ret.closeCode = WebSocketCloseCode::UnsupportedFeature;
ret.closeReason = "Parallel request batch processing is not available on this system due to limited core count.";
}
return;
}
@ -255,16 +250,16 @@ void WebSocketServer::ProcessMessage(SessionPtr session, WebSocketServer::Proces
if (payloadData.contains("variables") && !payloadData["variables"].is_null()) {
if (!payloadData.is_object()) {
if (!session->IgnoreInvalidMessages()) {
ret.closeCode = WebSocketCloseCode::InvalidDataKeyType;
ret.closeCode = WebSocketCloseCode::InvalidDataFieldType;
ret.closeReason = "Your `variables` is not an object.";
}
return;
}
if (executionType == OBS_WEBSOCKET_REQUEST_BATCH_EXECUTION_TYPE_PARALLEL) {
if (executionType == RequestBatchExecutionType::Parallel) {
if (!session->IgnoreInvalidMessages()) {
ret.closeCode = WebSocketCloseCode::UnsupportedFeature;
ret.closeReason = "Variables are not supported in PARALLEL mode.";
ret.closeReason = "Variables are not supported in Parallel mode.";
}
return;
}

View File

@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#include <util/profiler.h>
#include <util/profiler.hpp>
#include "WebSocketServer.h"
#include "../requesthandler/RequestHandler.h"
@ -31,7 +31,7 @@ struct SerialFrameRequest
const json outputVariables;
SerialFrameRequest(const std::string &requestType, const json &requestData, const json &inputVariables, const json &outputVariables) :
request(requestType, requestData, OBS_WEBSOCKET_REQUEST_BATCH_EXECUTION_TYPE_SERIAL_FRAME),
request(requestType, requestData, RequestBatchExecutionType::SerialFrame),
inputVariables(inputVariables),
outputVariables(outputVariables)
{}
@ -145,7 +145,7 @@ json ConstructRequestResult(RequestResult requestResult, const json &requestJson
void ObsTickCallback(void *param, float)
{
profile_start("obs-websocket-request-batch-frame-tick");
ScopeProfiler prof{"obs_websocket_request_batch_frame_tick"};
auto serialFrameBatch = reinterpret_cast<SerialFrameBatch*>(param);
@ -155,7 +155,6 @@ void ObsTickCallback(void *param, float)
if (serialFrameBatch->sleepUntilFrame) {
if (serialFrameBatch->frameCount < serialFrameBatch->sleepUntilFrame) {
// Do not process any requests if in "sleep mode"
profile_end("obs-websocket-request-batch-frame-tick");
return;
} else {
// Reset frame sleep until counter if not being used
@ -189,17 +188,15 @@ void ObsTickCallback(void *param, float)
if (serialFrameBatch->requests.empty()) {
serialFrameBatch->condition.notify_one();
}
profile_end("obs-websocket-request-batch-frame-tick");
}
void WebSocketServer::ProcessRequestBatch(SessionPtr session, ObsWebSocketRequestBatchExecutionType executionType, const std::vector<json> &requests, std::vector<json> &results, json &variables)
void WebSocketServer::ProcessRequestBatch(SessionPtr session, RequestBatchExecutionType::RequestBatchExecutionType executionType, const std::vector<json> &requests, std::vector<json> &results, json &variables)
{
RequestHandler requestHandler(session);
if (executionType == OBS_WEBSOCKET_REQUEST_BATCH_EXECUTION_TYPE_SERIAL_REALTIME) {
if (executionType == RequestBatchExecutionType::SerialRealtime) {
// Recurse all requests in batch serially, processing the request then moving to the next one
for (auto requestJson : requests) {
Request request(requestJson["requestType"], requestJson["requestData"], OBS_WEBSOCKET_REQUEST_BATCH_EXECUTION_TYPE_SERIAL_REALTIME);
Request request(requestJson["requestType"], requestJson["requestData"], RequestBatchExecutionType::SerialRealtime);
request.HasRequestData = PreProcessVariables(variables, requestJson["inputVariables"], request.RequestData);
@ -211,7 +208,7 @@ void WebSocketServer::ProcessRequestBatch(SessionPtr session, ObsWebSocketReques
results.push_back(result);
}
} else if (executionType == OBS_WEBSOCKET_REQUEST_BATCH_EXECUTION_TYPE_SERIAL_FRAME) {
} else if (executionType == RequestBatchExecutionType::SerialFrame) {
SerialFrameBatch serialFrameBatch(requestHandler, variables);
// Create Request objects in the worker thread (avoid unnecessary processing in graphics thread)
@ -236,13 +233,13 @@ void WebSocketServer::ProcessRequestBatch(SessionPtr session, ObsWebSocketReques
results.push_back(ConstructRequestResult(requestResult, requests[i]));
i++;
}
} else if (executionType == OBS_WEBSOCKET_REQUEST_BATCH_EXECUTION_TYPE_PARALLEL) {
} else if (executionType == RequestBatchExecutionType::Parallel) {
ParallelBatchResults parallelResults(requestHandler, requests.size());
// Submit each request as a task to the thread pool to be processed ASAP
for (auto requestJson : requests) {
_threadPool.start(Utils::Compat::CreateFunctionRunnable([&parallelResults, &executionType, requestJson]() {
Request request(requestJson["requestType"], requestJson["requestData"], OBS_WEBSOCKET_REQUEST_BATCH_EXECUTION_TYPE_PARALLEL);
Request request(requestJson["requestType"], requestJson["requestData"], RequestBatchExecutionType::Parallel);
RequestResult requestResult = parallelResults.requestHandler.ProcessRequest(request);

View File

@ -21,31 +21,152 @@ with this program. If not, see <https://www.gnu.org/licenses/>
namespace WebSocketCloseCode {
enum WebSocketCloseCode {
// Internal only
/**
* For internal use only to tell the request handler not to perform any close action.
*
* @enumIdentifier DontClose
* @enumValue 0
* @enumType WebSocketCloseCode
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
DontClose = 0,
// Reserved
/**
* Unknown reason, should never be used.
*
* @enumIdentifier UnknownReason
* @enumValue 4000
* @enumType WebSocketCloseCode
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
UnknownReason = 4000,
// The server was unable to decode the incoming websocket message
/**
* The server was unable to decode the incoming websocket message.
*
* @enumIdentifier MessageDecodeError
* @enumValue 4002
* @enumType WebSocketCloseCode
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
MessageDecodeError = 4002,
// A data key is missing but required
MissingDataKey = 4003,
// A data key has an invalid type
InvalidDataKeyType = 4004,
// The specified `op` was invalid or missing
UnknownOpCode = 4005,
// The client sent a websocket message without first sending `Identify` message
NotIdentified = 4006,
// The client sent an `Identify` message while already identified
AlreadyIdentified = 4007,
// The authentication attempt (via `Identify`) failed
AuthenticationFailed = 4008,
// The server detected the usage of an old version of the obs-websocket RPC protocol.
UnsupportedRpcVersion = 4009,
// The websocket session has been invalidated by the obs-websocket server.
SessionInvalidated = 4010,
// A data key's value is invalid, in the case of things like enums.
InvalidDataKeyValue = 4011,
// A feature is not supported because of hardware/software limitations.
/**
* A data field is required but missing from the payload.
*
* @enumIdentifier MissingDataField
* @enumValue 4003
* @enumType WebSocketCloseCode
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
MissingDataField = 4003,
/**
* A data field's value type is invalid.
*
* @enumIdentifier InvalidDataFieldType
* @enumValue 4004
* @enumType WebSocketCloseCode
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
InvalidDataFieldType = 4004,
/**
* A data field's value is invalid.
*
* @enumIdentifier InvalidDataFieldValue
* @enumValue 4005
* @enumType WebSocketCloseCode
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
InvalidDataFieldValue = 4005,
/**
* The specified `op` was invalid or missing.
*
* @enumIdentifier UnknownOpCode
* @enumValue 4006
* @enumType WebSocketCloseCode
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
UnknownOpCode = 4006,
/**
* The client sent a websocket message without first sending `Identify` message.
*
* @enumIdentifier NotIdentified
* @enumValue 4007
* @enumType WebSocketCloseCode
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
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.
*
* @enumIdentifier AlreadyIdentified
* @enumValue 4008
* @enumType WebSocketCloseCode
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
AlreadyIdentified = 4008,
/**
* The authentication attempt (via `Identify`) failed.
*
* @enumIdentifier AuthenticationFailed
* @enumValue 4009
* @enumType WebSocketCloseCode
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
AuthenticationFailed = 4009,
/**
* The server detected the usage of an old version of the obs-websocket RPC protocol.
*
* @enumIdentifier UnsupportedRpcVersion
* @enumValue 4010
* @enumType WebSocketCloseCode
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
UnsupportedRpcVersion = 4010,
/**
* The websocket 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.
*
* @enumIdentifier SessionInvalidated
* @enumValue 4011
* @enumType WebSocketCloseCode
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
SessionInvalidated = 4011,
/**
* A requested feature is not supported due to hardware/software limitations.
*
* @enumIdentifier UnsupportedFeature
* @enumValue 4012
* @enumType WebSocketCloseCode
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
UnsupportedFeature = 4012,
};
}

View File

@ -21,14 +21,104 @@ with this program. If not, see <https://www.gnu.org/licenses/>
namespace WebSocketOpCode {
enum WebSocketOpCode: uint8_t {
/**
* The initial message sent by obs-websocket to newly connected clients.
*
* @enumIdentifier Hello
* @enumValue 0
* @enumType WebSocketOpCode
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
Hello = 0,
/**
* The message sent by a newly connected client to obs-websocket in response to a `Hello`.
*
* @enumIdentifier Identify
* @enumValue 1
* @enumType WebSocketOpCode
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
Identify = 1,
/**
* The response sent by obs-websocket to a client after it has successfully identified with obs-websocket.
*
* @enumIdentifier Identified
* @enumValue 2
* @enumType WebSocketOpCode
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
Identified = 2,
/**
* The message sent by an already-identified client to update identification parameters.
*
* @enumIdentifier Reidentify
* @enumValue 3
* @enumType WebSocketOpCode
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
Reidentify = 3,
/**
* The message sent by obs-websocket containing an event payload.
*
* @enumIdentifier Event
* @enumValue 5
* @enumType WebSocketOpCode
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
Event = 5,
/**
* The message sent by a client to obs-websocket to perform a request.
*
* @enumIdentifier Request
* @enumValue 6
* @enumType WebSocketOpCode
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
Request = 6,
/**
* The message sent by obs-websocket in response to a particular request from a client.
*
* @enumIdentifier RequestResponse
* @enumValue 7
* @enumType WebSocketOpCode
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
RequestResponse = 7,
/**
* The message sent by a client to obs-websocket to perform a batch of requests.
*
* @enumIdentifier RequestBatch
* @enumValue 8
* @enumType WebSocketOpCode
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
RequestBatch = 8,
/**
* The message sent by obs-websocket in response to a particular batch of requests from a client.
*
* @enumIdentifier RequestBatchResponse
* @enumValue 9
* @enumType WebSocketOpCode
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
RequestBatchResponse = 9,
};
}