mirror of
https://github.com/Palakis/obs-websocket.git
synced 2024-08-30 18:12:16 +00:00
Compare commits
111 Commits
Author | SHA1 | Date | |
---|---|---|---|
9a8f248a75 | |||
6845cda146 | |||
4488b2b7a5 | |||
7dd8cb5d8d | |||
ebf631a15f | |||
3770b75a78 | |||
5d81b61325 | |||
a8e5171b40 | |||
a3e0abb6ce | |||
c51f20eb99 | |||
be897c4df2 | |||
11b948fb69 | |||
070660848b | |||
61af0ec9c6 | |||
897f115363 | |||
b8fcf0355c | |||
5bdff87e3f | |||
31991a3567 | |||
25c369d422 | |||
8d396b1518 | |||
ba327b746a | |||
7d11f912c9 | |||
919cdfb5a9 | |||
da9dd6f775 | |||
8b841f026e | |||
5342b39640 | |||
fc08add504 | |||
20379ac61e | |||
af40aa59ab | |||
f9afc5597a | |||
40178e4661 | |||
ad56abd6f5 | |||
9073a09d84 | |||
2a4e6fce3f | |||
0a3a407217 | |||
9ca9cf80df | |||
6c881a5da7 | |||
5322acd58c | |||
ce489e53c6 | |||
0c95e509dc | |||
46a006ed54 | |||
0cc9378d05 | |||
f3edb2ee68 | |||
a539f23194 | |||
6b6dec79c5 | |||
d7f5c042bf | |||
2fe9812b26 | |||
1c15c3c7a3 | |||
1ee5857139 | |||
2f9c2b0d65 | |||
6e5161f43b | |||
d492d74a64 | |||
26506731c5 | |||
b0c03d4f47 | |||
3bc8348c32 | |||
a034fbba83 | |||
73b5261c49 | |||
cee0cbebc2 | |||
9723147429 | |||
dca385ae87 | |||
71f792944c | |||
6041c4acd2 | |||
0c7529705e | |||
63f1ea1cec | |||
4bbaa75f41 | |||
5fe9314b74 | |||
bfd26493ab | |||
5d74d5d03e | |||
3142e097a5 | |||
b25d8d8201 | |||
7e68cd016a | |||
9e4fc9eb73 | |||
fb0d13a171 | |||
f097b36c2e | |||
57ce3cf80b | |||
c1da62391f | |||
d8882ce979 | |||
9c1871494e | |||
ac26c9114a | |||
fb0a57003c | |||
cda22233a6 | |||
add307577d | |||
4af46ac2b9 | |||
8d39752bda | |||
e3ff9c013e | |||
635870ba4b | |||
a827afb05b | |||
0195e13bdc | |||
76d8b688fd | |||
b4857e2c79 | |||
ac2ae90e8a | |||
0b5cb76b5d | |||
6eed9606ae | |||
29679ff94c | |||
65a1f8b746 | |||
48182d35bd | |||
b526ed47be | |||
2225dec2a9 | |||
011458d642 | |||
e32fff3f61 | |||
0b42e3d0f3 | |||
2b746d1353 | |||
6fed6f9f7d | |||
480945073a | |||
ef6df94838 | |||
fb05848426 | |||
e5be6b96fb | |||
2d706a245a | |||
1abc4f491b | |||
b21a45b43e | |||
84629f6e63 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -5,5 +5,5 @@
|
||||
/build64/
|
||||
/release/
|
||||
/installer/Output/
|
||||
|
||||
.idea
|
||||
.vscode
|
@ -3,7 +3,7 @@
|
||||
## Prerequisites
|
||||
|
||||
You'll need [Qt 5.10.x](https://download.qt.io/official_releases/qt/5.10/),
|
||||
[CMake](https://cmake.org/download/), [Boost](https://www.boost.org/) and a working [OBS Studio development environment](https://obsproject.com/wiki/install-instructions) installed on your
|
||||
[CMake](https://cmake.org/download/) and a working [OBS Studio development environment](https://obsproject.com/wiki/install-instructions) installed on your
|
||||
computer.
|
||||
|
||||
## Windows
|
||||
|
@ -29,11 +29,11 @@ if exist C:\projects\obs-studio\ (
|
||||
echo obs-studio directory exists
|
||||
echo Updating tag info
|
||||
cd C:\projects\obs-studio\
|
||||
git describe --tags --abbrev=0 > C:\projects\latest-obs-studio-tag-pre-pull.txt
|
||||
git describe --tags --abbrev=0 --exclude="*-rc*" > C:\projects\latest-obs-studio-tag-pre-pull.txt
|
||||
set /p OBSLatestTagPrePull=<C:\projects\latest-obs-studio-tag-pre-pull.txt
|
||||
git checkout master
|
||||
git pull
|
||||
git describe --tags --abbrev=0 > C:\projects\latest-obs-studio-tag-post-pull.txt
|
||||
git describe --tags --abbrev=0 --exclude="*-rc*" > C:\projects\latest-obs-studio-tag-post-pull.txt
|
||||
set /p OBSLatestTagPostPull=<C:\projects\latest-obs-studio-tag-post-pull.txt
|
||||
set /p OBSLatestTag=<C:\projects\latest-obs-studio-tag-post-pull.txt
|
||||
echo %OBSLatestTagPostPull%> C:\projects\latest-obs-studio-tag.txt
|
||||
@ -62,7 +62,7 @@ if not exist C:\projects\obs-studio (
|
||||
echo obs-studio directory does not exist
|
||||
git clone https://github.com/obsproject/obs-studio
|
||||
cd C:\projects\obs-studio\
|
||||
git describe --tags --abbrev=0 > C:\projects\obs-studio-latest-tag.txt
|
||||
git describe --tags --abbrev=0 --exclude="*-rc*" > C:\projects\obs-studio-latest-tag.txt
|
||||
set /p OBSLatestTag=<C:\projects\obs-studio-latest-tag.txt
|
||||
set BuildOBS=true
|
||||
)
|
||||
|
@ -518,7 +518,7 @@
|
||||
<key>OVERWRITE_PERMISSIONS</key>
|
||||
<false/>
|
||||
<key>VERSION</key>
|
||||
<string>4.6.1</string>
|
||||
<string>4.7.0</string>
|
||||
</dict>
|
||||
<key>PROJECT_COMMENTS</key>
|
||||
<dict>
|
||||
|
@ -23,6 +23,9 @@ export LATEST_FILENAME="obs-websocket-latest-$LATEST_VERSION.pkg"
|
||||
|
||||
echo "[obs-websocket] Modifying obs-websocket.so"
|
||||
install_name_tool \
|
||||
-add_rpath @executable_path/../Frameworks/QtWidgets.framework/Versions/5/ \
|
||||
-add_rpath @executable_path/../Frameworks/QtGui.framework/Versions/5/ \
|
||||
-add_rpath @executable_path/../Frameworks/QtCore.framework/Versions/5/ \
|
||||
-change /usr/local/opt/qt/lib/QtWidgets.framework/Versions/5/QtWidgets @rpath/QtWidgets \
|
||||
-change /usr/local/opt/qt/lib/QtGui.framework/Versions/5/QtGui @rpath/QtGui \
|
||||
-change /usr/local/opt/qt/lib/QtCore.framework/Versions/5/QtCore @rpath/QtCore \
|
||||
|
@ -42,6 +42,7 @@ set(obs-websocket_SOURCES
|
||||
src/WSRequestHandler_Streaming.cpp
|
||||
src/WSRequestHandler_StudioMode.cpp
|
||||
src/WSRequestHandler_Transitions.cpp
|
||||
src/WSRequestHandler_Outputs.cpp
|
||||
src/WSEvents.cpp
|
||||
src/Config.cpp
|
||||
src/Utils.cpp
|
||||
|
11
README.md
11
README.md
@ -1,20 +1,24 @@
|
||||
obs-websocket
|
||||
==============
|
||||
Remote control of OBS Studio made easy.
|
||||
|
||||
WebSockets API for OBS Studio.
|
||||
|
||||
Follow the main author on Twitter for news & updates : [@LePalakis](https://twitter.com/LePalakis)
|
||||
|
||||
[](https://ci.appveyor.com/project/Palakis/obs-websocket/history) [](https://travis-ci.org/Palakis/obs-websocket)
|
||||
[](https://ci.appveyor.com/project/Palakis/obs-websocket/history) [](https://travis-ci.org/Palakis/obs-websocket)
|
||||
|
||||
## Downloads
|
||||
|
||||
Binaries for Windows and Linux are available in the [Releases](https://github.com/Palakis/obs-websocket/releases) section.
|
||||
|
||||
## Using obs-websocket
|
||||
|
||||
A web client and frontend made by [t2t2](https://github.com/t2t2/obs-tablet-remote) (compatible with tablets and other touch interfaces) is available here : http://t2t2.github.io/obs-tablet-remote/
|
||||
|
||||
It is **highly recommended** to protect obs-websocket with a password against unauthorized control. To do this, open the "Websocket server settings" dialog under OBS' "Tools" menu. In the settings dialogs, you can enable or disable authentication and set a password for it.
|
||||
|
||||
### Possible use cases
|
||||
|
||||
- Remote control OBS from a phone or tablet on the same local network
|
||||
- Change your stream overlay/graphics based on the current scene (like the AGDQ overlay does)
|
||||
- Automate scene switching with a third-party program (e.g. : auto-pilot, foot pedal, ...)
|
||||
@ -29,6 +33,7 @@ Here's a list of available language APIs for obs-websocket :
|
||||
- C#/VB.NET: [obs-websocket-dotnet](https://github.com/Palakis/obs-websocket-dotnet)
|
||||
- Python 2 and 3: [obs-websocket-py](https://github.com/Elektordi/obs-websocket-py) by Guillaume Genty a.k.a Elektordi
|
||||
- Python 3.5+ with asyncio: [obs-ws-rc](https://github.com/KirillMysnik/obs-ws-rc) by Kirill Mysnik
|
||||
- Java 8+: [obs-websocket-java](https://github.com/Twasi/websocket-obs-java) by TwasiNET
|
||||
|
||||
I'd like to know what you're building with or for obs-websocket. If you do something in this fashion, feel free to drop me an email at `stephane /dot/ lepin /at/ gmail /dot/ com` !
|
||||
|
||||
@ -59,7 +64,7 @@ If your Pull Request is not ready to merge yet, tag it with the `work in progres
|
||||
Source code is indented with tabs, with spaces allowed for alignment.
|
||||
|
||||
Regarding protocol changes: new and updated request types / events must always come with accompanying documentation comments (see existing protocol elements for examples).
|
||||
These are using to automatically generate the [protocol specification](docs/generated/protocol.md).
|
||||
These are required to automatically generate the [protocol specification document](docs/generated/protocol.md).
|
||||
|
||||
Among other recommendations: favor return-early code and avoid wrapping huge portions of code in conditionals. As an example, this:
|
||||
|
||||
|
@ -333,6 +333,135 @@
|
||||
},
|
||||
"examples": []
|
||||
},
|
||||
{
|
||||
"subheads": [],
|
||||
"typedef": "{Object} `Output`",
|
||||
"property": [
|
||||
"{String} `name` Output name",
|
||||
"{String} `type` Output type/kind",
|
||||
"{int} `width` Video output width",
|
||||
"{int} `height` Video output height",
|
||||
"{Object} `flags` Output flags",
|
||||
"{int} `flags.rawValue` Raw flags value",
|
||||
"{boolean} `flags.audio` Output uses audio",
|
||||
"{boolean} `flags.video` Output uses video",
|
||||
"{boolean} `flags.encoded` Output is encoded",
|
||||
"{boolean} `flags.multiTrack` Output uses several audio tracks",
|
||||
"{boolean} `flags.service` Output uses a service",
|
||||
"{Object} `settings` Output name",
|
||||
"{boolean} `active` Output status (active or not)",
|
||||
"{boolean} `reconnecting` Output reconnection status (reconnecting or not)",
|
||||
"{double} `congestion` Output congestion",
|
||||
"{int} `totalFrames` Number of frames sent",
|
||||
"{int} `droppedFrames` Number of frames dropped",
|
||||
"{int} `totalBytes` Total bytes sent"
|
||||
],
|
||||
"properties": [
|
||||
{
|
||||
"type": "String",
|
||||
"name": "name",
|
||||
"description": "Output name"
|
||||
},
|
||||
{
|
||||
"type": "String",
|
||||
"name": "type",
|
||||
"description": "Output type/kind"
|
||||
},
|
||||
{
|
||||
"type": "int",
|
||||
"name": "width",
|
||||
"description": "Video output width"
|
||||
},
|
||||
{
|
||||
"type": "int",
|
||||
"name": "height",
|
||||
"description": "Video output height"
|
||||
},
|
||||
{
|
||||
"type": "Object",
|
||||
"name": "flags",
|
||||
"description": "Output flags"
|
||||
},
|
||||
{
|
||||
"type": "int",
|
||||
"name": "flags.rawValue",
|
||||
"description": "Raw flags value"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"name": "flags.audio",
|
||||
"description": "Output uses audio"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"name": "flags.video",
|
||||
"description": "Output uses video"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"name": "flags.encoded",
|
||||
"description": "Output is encoded"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"name": "flags.multiTrack",
|
||||
"description": "Output uses several audio tracks"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"name": "flags.service",
|
||||
"description": "Output uses a service"
|
||||
},
|
||||
{
|
||||
"type": "Object",
|
||||
"name": "settings",
|
||||
"description": "Output name"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"name": "active",
|
||||
"description": "Output status (active or not)"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"name": "reconnecting",
|
||||
"description": "Output reconnection status (reconnecting or not)"
|
||||
},
|
||||
{
|
||||
"type": "double",
|
||||
"name": "congestion",
|
||||
"description": "Output congestion"
|
||||
},
|
||||
{
|
||||
"type": "int",
|
||||
"name": "totalFrames",
|
||||
"description": "Number of frames sent"
|
||||
},
|
||||
{
|
||||
"type": "int",
|
||||
"name": "droppedFrames",
|
||||
"description": "Number of frames dropped"
|
||||
},
|
||||
{
|
||||
"type": "int",
|
||||
"name": "totalBytes",
|
||||
"description": "Total bytes sent"
|
||||
}
|
||||
],
|
||||
"typedefs": [
|
||||
{
|
||||
"type": "Object",
|
||||
"name": "Output",
|
||||
"description": ""
|
||||
}
|
||||
],
|
||||
"name": "",
|
||||
"heading": {
|
||||
"level": 2,
|
||||
"text": ""
|
||||
},
|
||||
"examples": []
|
||||
},
|
||||
{
|
||||
"subheads": [],
|
||||
"typedef": "{Object} `Scene`",
|
||||
@ -1197,6 +1326,72 @@
|
||||
"lead": "",
|
||||
"type": "class",
|
||||
"examples": []
|
||||
},
|
||||
{
|
||||
"subheads": [],
|
||||
"description": "Current recording paused",
|
||||
"api": "events",
|
||||
"name": "RecordingPaused",
|
||||
"category": "recording",
|
||||
"since": "4.7.0",
|
||||
"names": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "RecordingPaused"
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "recording"
|
||||
}
|
||||
],
|
||||
"sinces": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "4.7.0"
|
||||
}
|
||||
],
|
||||
"heading": {
|
||||
"level": 2,
|
||||
"text": "RecordingPaused"
|
||||
},
|
||||
"lead": "",
|
||||
"type": "class",
|
||||
"examples": []
|
||||
},
|
||||
{
|
||||
"subheads": [],
|
||||
"description": "Current recording resumed",
|
||||
"api": "events",
|
||||
"name": "RecordingResumed",
|
||||
"category": "recording",
|
||||
"since": "4.7.0",
|
||||
"names": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "RecordingResumed"
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "recording"
|
||||
}
|
||||
],
|
||||
"sinces": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "4.7.0"
|
||||
}
|
||||
],
|
||||
"heading": {
|
||||
"level": 2,
|
||||
"text": "RecordingResumed"
|
||||
},
|
||||
"lead": "",
|
||||
"type": "class",
|
||||
"examples": []
|
||||
}
|
||||
],
|
||||
"replay buffer": [
|
||||
@ -1470,6 +1665,55 @@
|
||||
"lead": "",
|
||||
"type": "class",
|
||||
"examples": []
|
||||
},
|
||||
{
|
||||
"subheads": [],
|
||||
"description": "A custom broadcast message was received",
|
||||
"return": [
|
||||
"{String} `realm` Identifier provided by the sender",
|
||||
"{Object} `data` User-defined data"
|
||||
],
|
||||
"api": "events",
|
||||
"name": "BroadcastCustomMessage",
|
||||
"category": "general",
|
||||
"since": "4.7.0",
|
||||
"returns": [
|
||||
{
|
||||
"type": "String",
|
||||
"name": "realm",
|
||||
"description": "Identifier provided by the sender"
|
||||
},
|
||||
{
|
||||
"type": "Object",
|
||||
"name": "data",
|
||||
"description": "User-defined data"
|
||||
}
|
||||
],
|
||||
"names": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "BroadcastCustomMessage"
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "general"
|
||||
}
|
||||
],
|
||||
"sinces": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "4.7.0"
|
||||
}
|
||||
],
|
||||
"heading": {
|
||||
"level": 2,
|
||||
"text": "BroadcastCustomMessage"
|
||||
},
|
||||
"lead": "",
|
||||
"type": "class",
|
||||
"examples": []
|
||||
}
|
||||
],
|
||||
"sources": [
|
||||
@ -1741,9 +1985,9 @@
|
||||
"description": "Audio mixer routing changed on a source.",
|
||||
"return": [
|
||||
"{String} `sourceName` Source name",
|
||||
"{Array<Object>} `routingStatus` Routing status of the source for each audio mixer (array of 6 values)",
|
||||
"{int} `routingStatus.*.id` Mixer number",
|
||||
"{boolean} `routingStatus.*.enabled` Routing status",
|
||||
"{Array<Object>} `mixers` Routing status of the source for each audio mixer (array of 6 values)",
|
||||
"{int} `mixers.*.id` Mixer number",
|
||||
"{boolean} `mixers.*.enabled` Routing status",
|
||||
"{String} `hexMixersValue` Raw mixer flags (little-endian, one bit per mixer) as an hexadecimal value"
|
||||
],
|
||||
"api": "events",
|
||||
@ -1758,17 +2002,17 @@
|
||||
},
|
||||
{
|
||||
"type": "Array<Object>",
|
||||
"name": "routingStatus",
|
||||
"name": "mixers",
|
||||
"description": "Routing status of the source for each audio mixer (array of 6 values)"
|
||||
},
|
||||
{
|
||||
"type": "int",
|
||||
"name": "routingStatus.*.id",
|
||||
"name": "mixers.*.id",
|
||||
"description": "Mixer number"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"name": "routingStatus.*.enabled",
|
||||
"name": "mixers.*.enabled",
|
||||
"description": "Routing status"
|
||||
},
|
||||
{
|
||||
@ -1968,6 +2212,61 @@
|
||||
"type": "class",
|
||||
"examples": []
|
||||
},
|
||||
{
|
||||
"subheads": [],
|
||||
"description": "The visibility/enabled state of a filter changed",
|
||||
"return": [
|
||||
"{String} `sourceName` Source name",
|
||||
"{String} `filterName` Filter name",
|
||||
"{Boolean} `filterEnabled` New filter state"
|
||||
],
|
||||
"api": "events",
|
||||
"name": "SourceFilterVisibilityChanged",
|
||||
"category": "sources",
|
||||
"since": "4.7.0",
|
||||
"returns": [
|
||||
{
|
||||
"type": "String",
|
||||
"name": "sourceName",
|
||||
"description": "Source name"
|
||||
},
|
||||
{
|
||||
"type": "String",
|
||||
"name": "filterName",
|
||||
"description": "Filter name"
|
||||
},
|
||||
{
|
||||
"type": "Boolean",
|
||||
"name": "filterEnabled",
|
||||
"description": "New filter state"
|
||||
}
|
||||
],
|
||||
"names": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "SourceFilterVisibilityChanged"
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "sources"
|
||||
}
|
||||
],
|
||||
"sinces": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "4.7.0"
|
||||
}
|
||||
],
|
||||
"heading": {
|
||||
"level": 2,
|
||||
"text": "SourceFilterVisibilityChanged"
|
||||
},
|
||||
"lead": "",
|
||||
"type": "class",
|
||||
"examples": []
|
||||
},
|
||||
{
|
||||
"subheads": [],
|
||||
"description": "Filters in a source have been reordered.",
|
||||
@ -2849,6 +3148,55 @@
|
||||
"type": "class",
|
||||
"examples": []
|
||||
},
|
||||
{
|
||||
"subheads": [],
|
||||
"description": "Broadcast custom message to all connected WebSocket clients",
|
||||
"param": [
|
||||
"{String} `realm` Identifier to be choosen by the client",
|
||||
"{Object} `data` User-defined data"
|
||||
],
|
||||
"api": "requests",
|
||||
"name": "BroadcastCustomMessage",
|
||||
"category": "general",
|
||||
"since": "4.7.0",
|
||||
"params": [
|
||||
{
|
||||
"type": "String",
|
||||
"name": "realm",
|
||||
"description": "Identifier to be choosen by the client"
|
||||
},
|
||||
{
|
||||
"type": "Object",
|
||||
"name": "data",
|
||||
"description": "User-defined data"
|
||||
}
|
||||
],
|
||||
"names": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "BroadcastCustomMessage"
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "general"
|
||||
}
|
||||
],
|
||||
"sinces": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "4.7.0"
|
||||
}
|
||||
],
|
||||
"heading": {
|
||||
"level": 2,
|
||||
"text": "BroadcastCustomMessage"
|
||||
},
|
||||
"lead": "",
|
||||
"type": "class",
|
||||
"examples": []
|
||||
},
|
||||
{
|
||||
"subheads": [],
|
||||
"description": "Get basic OBS video information",
|
||||
@ -2941,6 +3289,188 @@
|
||||
"examples": []
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"subheads": [],
|
||||
"description": "List existing outputs",
|
||||
"return": "{Array<Output>} `outputs` Outputs list",
|
||||
"api": "requests",
|
||||
"name": "ListOutputs",
|
||||
"category": "outputs",
|
||||
"since": "4.7.0",
|
||||
"returns": [
|
||||
{
|
||||
"type": "Array<Output>",
|
||||
"name": "outputs",
|
||||
"description": "Outputs list"
|
||||
}
|
||||
],
|
||||
"names": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "ListOutputs"
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "outputs"
|
||||
}
|
||||
],
|
||||
"sinces": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "4.7.0"
|
||||
}
|
||||
],
|
||||
"heading": {
|
||||
"level": 2,
|
||||
"text": "ListOutputs"
|
||||
},
|
||||
"lead": "",
|
||||
"type": "class",
|
||||
"examples": []
|
||||
},
|
||||
{
|
||||
"subheads": [],
|
||||
"description": "Get information about a single output",
|
||||
"param": "{String} `outputName` Output name",
|
||||
"return": "{Output} `outputInfo` Output info",
|
||||
"api": "requests",
|
||||
"name": "GetOutputInfo",
|
||||
"category": "outputs",
|
||||
"since": "4.7.0",
|
||||
"returns": [
|
||||
{
|
||||
"type": "Output",
|
||||
"name": "outputInfo",
|
||||
"description": "Output info"
|
||||
}
|
||||
],
|
||||
"params": [
|
||||
{
|
||||
"type": "String",
|
||||
"name": "outputName",
|
||||
"description": "Output name"
|
||||
}
|
||||
],
|
||||
"names": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "GetOutputInfo"
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "outputs"
|
||||
}
|
||||
],
|
||||
"sinces": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "4.7.0"
|
||||
}
|
||||
],
|
||||
"heading": {
|
||||
"level": 2,
|
||||
"text": "GetOutputInfo"
|
||||
},
|
||||
"lead": "",
|
||||
"type": "class",
|
||||
"examples": []
|
||||
},
|
||||
{
|
||||
"subheads": [],
|
||||
"description": "Start an output",
|
||||
"param": "{String} `outputName` Output name",
|
||||
"api": "requests",
|
||||
"name": "StartOutput",
|
||||
"category": "outputs",
|
||||
"since": "4.7.0",
|
||||
"params": [
|
||||
{
|
||||
"type": "String",
|
||||
"name": "outputName",
|
||||
"description": "Output name"
|
||||
}
|
||||
],
|
||||
"names": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "StartOutput"
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "outputs"
|
||||
}
|
||||
],
|
||||
"sinces": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "4.7.0"
|
||||
}
|
||||
],
|
||||
"heading": {
|
||||
"level": 2,
|
||||
"text": "StartOutput"
|
||||
},
|
||||
"lead": "",
|
||||
"type": "class",
|
||||
"examples": []
|
||||
},
|
||||
{
|
||||
"subheads": [],
|
||||
"description": "Stop an output",
|
||||
"param": [
|
||||
"{String} `outputName` Output name",
|
||||
"{boolean (optional)} `force` Force stop (default: false)"
|
||||
],
|
||||
"api": "requests",
|
||||
"name": "StopOutput",
|
||||
"category": "outputs",
|
||||
"since": "4.7.0",
|
||||
"params": [
|
||||
{
|
||||
"type": "String",
|
||||
"name": "outputName",
|
||||
"description": "Output name"
|
||||
},
|
||||
{
|
||||
"type": "boolean (optional)",
|
||||
"name": "force",
|
||||
"description": "Force stop (default: false)"
|
||||
}
|
||||
],
|
||||
"names": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "StopOutput"
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "outputs"
|
||||
}
|
||||
],
|
||||
"sinces": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "4.7.0"
|
||||
}
|
||||
],
|
||||
"heading": {
|
||||
"level": 2,
|
||||
"text": "StopOutput"
|
||||
},
|
||||
"lead": "",
|
||||
"type": "class",
|
||||
"examples": []
|
||||
}
|
||||
],
|
||||
"profiles": [
|
||||
{
|
||||
"subheads": [],
|
||||
@ -3166,6 +3696,72 @@
|
||||
"type": "class",
|
||||
"examples": []
|
||||
},
|
||||
{
|
||||
"subheads": [],
|
||||
"description": "Pause the current recording.\nReturns an error if recording is not active or already paused.",
|
||||
"api": "requests",
|
||||
"name": "PauseRecording",
|
||||
"category": "recording",
|
||||
"since": "4.7.0",
|
||||
"names": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "PauseRecording"
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "recording"
|
||||
}
|
||||
],
|
||||
"sinces": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "4.7.0"
|
||||
}
|
||||
],
|
||||
"heading": {
|
||||
"level": 2,
|
||||
"text": "PauseRecording"
|
||||
},
|
||||
"lead": "",
|
||||
"type": "class",
|
||||
"examples": []
|
||||
},
|
||||
{
|
||||
"subheads": [],
|
||||
"description": "Resume/unpause the current recording (if paused).\nReturns an error if recording is not active or not paused.",
|
||||
"api": "requests",
|
||||
"name": "ResumeRecording",
|
||||
"category": "recording",
|
||||
"since": "4.7.0",
|
||||
"names": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "ResumeRecording"
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "recording"
|
||||
}
|
||||
],
|
||||
"sinces": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "4.7.0"
|
||||
}
|
||||
],
|
||||
"heading": {
|
||||
"level": 2,
|
||||
"text": "ResumeRecording"
|
||||
},
|
||||
"lead": "",
|
||||
"type": "class",
|
||||
"examples": []
|
||||
},
|
||||
{
|
||||
"subheads": [],
|
||||
"description": "\n\nPlease note: if `SetRecordingFolder` is called while a recording is\nin progress, the change won't be applied immediately and will be\neffective on the next recording.",
|
||||
@ -4607,19 +5203,19 @@
|
||||
"subheads": [],
|
||||
"description": "Get a list of all available sources types",
|
||||
"return": [
|
||||
"{Array<Object>} `ids` Array of source types",
|
||||
"{String} `ids.*.typeId` Non-unique internal source type ID",
|
||||
"{String} `ids.*.displayName` Display name of the source type",
|
||||
"{String} `ids.*.type` Type. Value is one of the following: \"input\", \"filter\", \"transition\" or \"other\"",
|
||||
"{Object} `ids.*.defaultSettings` Default settings of this source type",
|
||||
"{Object} `ids.*.caps` Source type capabilities",
|
||||
"{Boolean} `ids.*.caps.isAsync` True if source of this type provide frames asynchronously",
|
||||
"{Boolean} `ids.*.caps.hasVideo` True if sources of this type provide video",
|
||||
"{Boolean} `ids.*.caps.hasAudio` True if sources of this type provide audio",
|
||||
"{Boolean} `ids.*.caps.canInteract` True if interaction with this sources of this type is possible",
|
||||
"{Boolean} `ids.*.caps.isComposite` True if sources of this type composite one or more sub-sources",
|
||||
"{Boolean} `ids.*.caps.doNotDuplicate` True if sources of this type should not be fully duplicated",
|
||||
"{Boolean} `ids.*.caps.doNotSelfMonitor` True if sources of this type may cause a feedback loop if it's audio is monitored and shouldn't be"
|
||||
"{Array<Object>} `types` Array of source types",
|
||||
"{String} `types.*.typeId` Non-unique internal source type ID",
|
||||
"{String} `types.*.displayName` Display name of the source type",
|
||||
"{String} `types.*.type` Type. Value is one of the following: \"input\", \"filter\", \"transition\" or \"other\"",
|
||||
"{Object} `types.*.defaultSettings` Default settings of this source type",
|
||||
"{Object} `types.*.caps` Source type capabilities",
|
||||
"{Boolean} `types.*.caps.isAsync` True if source of this type provide frames asynchronously",
|
||||
"{Boolean} `types.*.caps.hasVideo` True if sources of this type provide video",
|
||||
"{Boolean} `types.*.caps.hasAudio` True if sources of this type provide audio",
|
||||
"{Boolean} `types.*.caps.canInteract` True if interaction with this sources of this type is possible",
|
||||
"{Boolean} `types.*.caps.isComposite` True if sources of this type composite one or more sub-sources",
|
||||
"{Boolean} `types.*.caps.doNotDuplicate` True if sources of this type should not be fully duplicated",
|
||||
"{Boolean} `types.*.caps.doNotSelfMonitor` True if sources of this type may cause a feedback loop if it's audio is monitored and shouldn't be"
|
||||
],
|
||||
"api": "requests",
|
||||
"name": "GetSourceTypesList",
|
||||
@ -4628,67 +5224,67 @@
|
||||
"returns": [
|
||||
{
|
||||
"type": "Array<Object>",
|
||||
"name": "ids",
|
||||
"name": "types",
|
||||
"description": "Array of source types"
|
||||
},
|
||||
{
|
||||
"type": "String",
|
||||
"name": "ids.*.typeId",
|
||||
"name": "types.*.typeId",
|
||||
"description": "Non-unique internal source type ID"
|
||||
},
|
||||
{
|
||||
"type": "String",
|
||||
"name": "ids.*.displayName",
|
||||
"name": "types.*.displayName",
|
||||
"description": "Display name of the source type"
|
||||
},
|
||||
{
|
||||
"type": "String",
|
||||
"name": "ids.*.type",
|
||||
"name": "types.*.type",
|
||||
"description": "Type. Value is one of the following: \"input\", \"filter\", \"transition\" or \"other\""
|
||||
},
|
||||
{
|
||||
"type": "Object",
|
||||
"name": "ids.*.defaultSettings",
|
||||
"name": "types.*.defaultSettings",
|
||||
"description": "Default settings of this source type"
|
||||
},
|
||||
{
|
||||
"type": "Object",
|
||||
"name": "ids.*.caps",
|
||||
"name": "types.*.caps",
|
||||
"description": "Source type capabilities"
|
||||
},
|
||||
{
|
||||
"type": "Boolean",
|
||||
"name": "ids.*.caps.isAsync",
|
||||
"name": "types.*.caps.isAsync",
|
||||
"description": "True if source of this type provide frames asynchronously"
|
||||
},
|
||||
{
|
||||
"type": "Boolean",
|
||||
"name": "ids.*.caps.hasVideo",
|
||||
"name": "types.*.caps.hasVideo",
|
||||
"description": "True if sources of this type provide video"
|
||||
},
|
||||
{
|
||||
"type": "Boolean",
|
||||
"name": "ids.*.caps.hasAudio",
|
||||
"name": "types.*.caps.hasAudio",
|
||||
"description": "True if sources of this type provide audio"
|
||||
},
|
||||
{
|
||||
"type": "Boolean",
|
||||
"name": "ids.*.caps.canInteract",
|
||||
"name": "types.*.caps.canInteract",
|
||||
"description": "True if interaction with this sources of this type is possible"
|
||||
},
|
||||
{
|
||||
"type": "Boolean",
|
||||
"name": "ids.*.caps.isComposite",
|
||||
"name": "types.*.caps.isComposite",
|
||||
"description": "True if sources of this type composite one or more sub-sources"
|
||||
},
|
||||
{
|
||||
"type": "Boolean",
|
||||
"name": "ids.*.caps.doNotDuplicate",
|
||||
"name": "types.*.caps.doNotDuplicate",
|
||||
"description": "True if sources of this type should not be fully duplicated"
|
||||
},
|
||||
{
|
||||
"type": "Boolean",
|
||||
"name": "ids.*.caps.doNotSelfMonitor",
|
||||
"name": "types.*.caps.doNotSelfMonitor",
|
||||
"description": "True if sources of this type may cause a feedback loop if it's audio is monitored and shouldn't be"
|
||||
}
|
||||
],
|
||||
@ -6198,6 +6794,7 @@
|
||||
"param": "{String} `sourceName` Source name",
|
||||
"return": [
|
||||
"{Array<Object>} `filters` List of filters for the specified source",
|
||||
"{Boolean} `filters.*.enabled` Filter status (enabled or not)",
|
||||
"{String} `filters.*.type` Filter type",
|
||||
"{String} `filters.*.name` Filter name",
|
||||
"{Object} `filters.*.settings` Filter settings"
|
||||
@ -6212,6 +6809,11 @@
|
||||
"name": "filters",
|
||||
"description": "List of filters for the specified source"
|
||||
},
|
||||
{
|
||||
"type": "Boolean",
|
||||
"name": "filters.*.enabled",
|
||||
"description": "Filter status (enabled or not)"
|
||||
},
|
||||
{
|
||||
"type": "String",
|
||||
"name": "filters.*.type",
|
||||
@ -6261,6 +6863,83 @@
|
||||
"type": "class",
|
||||
"examples": []
|
||||
},
|
||||
{
|
||||
"subheads": [],
|
||||
"description": "List filters applied to a source",
|
||||
"param": [
|
||||
"{String} `sourceName` Source name",
|
||||
"{String} `filterName` Source filter name"
|
||||
],
|
||||
"return": [
|
||||
"{Boolean} `enabled` Filter status (enabled or not)",
|
||||
"{String} `type` Filter type",
|
||||
"{String} `name` Filter name",
|
||||
"{Object} `settings` Filter settings"
|
||||
],
|
||||
"api": "requests",
|
||||
"name": "GetSourceFilterInfo",
|
||||
"category": "sources",
|
||||
"since": "4.7.0",
|
||||
"returns": [
|
||||
{
|
||||
"type": "Boolean",
|
||||
"name": "enabled",
|
||||
"description": "Filter status (enabled or not)"
|
||||
},
|
||||
{
|
||||
"type": "String",
|
||||
"name": "type",
|
||||
"description": "Filter type"
|
||||
},
|
||||
{
|
||||
"type": "String",
|
||||
"name": "name",
|
||||
"description": "Filter name"
|
||||
},
|
||||
{
|
||||
"type": "Object",
|
||||
"name": "settings",
|
||||
"description": "Filter settings"
|
||||
}
|
||||
],
|
||||
"params": [
|
||||
{
|
||||
"type": "String",
|
||||
"name": "sourceName",
|
||||
"description": "Source name"
|
||||
},
|
||||
{
|
||||
"type": "String",
|
||||
"name": "filterName",
|
||||
"description": "Source filter name"
|
||||
}
|
||||
],
|
||||
"names": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "GetSourceFilterInfo"
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "sources"
|
||||
}
|
||||
],
|
||||
"sinces": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "4.7.0"
|
||||
}
|
||||
],
|
||||
"heading": {
|
||||
"level": 2,
|
||||
"text": "GetSourceFilterInfo"
|
||||
},
|
||||
"lead": "",
|
||||
"type": "class",
|
||||
"examples": []
|
||||
},
|
||||
{
|
||||
"subheads": [],
|
||||
"description": "Add a new filter to a source. Available source types along with their settings properties are available from `GetSourceTypesList`.",
|
||||
@ -6538,9 +7217,64 @@
|
||||
},
|
||||
{
|
||||
"subheads": [],
|
||||
"description": "\n\nAt least `embedPictureFormat` or `saveToFilePath` must be specified.\n\nClients can specify `width` and `height` parameters to receive scaled pictures. Aspect ratio is\npreserved if only one of these two parameters is specified.",
|
||||
"description": "Change the visibility/enabled state of a filter",
|
||||
"param": [
|
||||
"{String} `sourceName` Source name",
|
||||
"{String} `filterName` Source filter name",
|
||||
"{String} `filterEnabled` New filter state"
|
||||
],
|
||||
"api": "requests",
|
||||
"name": "SetSourceFilterVisibility",
|
||||
"category": "sources",
|
||||
"since": "4.7.0",
|
||||
"params": [
|
||||
{
|
||||
"type": "String",
|
||||
"name": "sourceName",
|
||||
"description": "Source name"
|
||||
},
|
||||
{
|
||||
"type": "String",
|
||||
"name": "filterName",
|
||||
"description": "Source filter name"
|
||||
},
|
||||
{
|
||||
"type": "String",
|
||||
"name": "filterEnabled",
|
||||
"description": "New filter state"
|
||||
}
|
||||
],
|
||||
"names": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "SetSourceFilterVisibility"
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "sources"
|
||||
}
|
||||
],
|
||||
"sinces": [
|
||||
{
|
||||
"name": "",
|
||||
"description": "4.7.0"
|
||||
}
|
||||
],
|
||||
"heading": {
|
||||
"level": 2,
|
||||
"text": "SetSourceFilterVisibility"
|
||||
},
|
||||
"lead": "",
|
||||
"type": "class",
|
||||
"examples": []
|
||||
},
|
||||
{
|
||||
"subheads": [],
|
||||
"description": "\n\nAt least `embedPictureFormat` or `saveToFilePath` must be specified.\n\nClients can specify `width` and `height` parameters to receive scaled pictures. Aspect ratio is\npreserved if only one of these two parameters is specified.",
|
||||
"param": [
|
||||
"{String} `sourceName` Source name. Note that, since scenes are also sources, you can also provide a scene name.",
|
||||
"{String (optional)} `embedPictureFormat` Format of the Data URI encoded picture. Can be \"png\", \"jpg\", \"jpeg\" or \"bmp\" (or any other value supported by Qt's Image module)",
|
||||
"{String (optional)} `saveToFilePath` Full file path (file extension included) where the captured image is to be saved. Can be in a format different from `pictureFormat`. Can be a relative path.",
|
||||
"{int (optional)} `width` Screenshot width. Defaults to the source's base width.",
|
||||
@ -6576,7 +7310,7 @@
|
||||
{
|
||||
"type": "String",
|
||||
"name": "sourceName",
|
||||
"description": "Source name"
|
||||
"description": "Source name. Note that, since scenes are also sources, you can also provide a scene name."
|
||||
},
|
||||
{
|
||||
"type": "String (optional)",
|
||||
|
@ -1,6 +1,6 @@
|
||||
<!-- This file was generated based on handlebars templates. Do not edit directly! -->
|
||||
|
||||
# obs-websocket 4.6.0 protocol reference
|
||||
# obs-websocket 4.7.0 protocol reference
|
||||
|
||||
# General Introduction
|
||||
Messages are exchanged between the client and the server as JSON objects.
|
||||
@ -46,6 +46,7 @@ auth_response = base64_encode(auth_response_hash)
|
||||
* [SceneItem](#sceneitem)
|
||||
* [SceneItemTransform](#sceneitemtransform)
|
||||
* [OBSStats](#obsstats)
|
||||
* [Output](#output)
|
||||
* [Scene](#scene)
|
||||
- [Events](#events)
|
||||
* [Scenes](#scenes)
|
||||
@ -72,6 +73,8 @@ auth_response = base64_encode(auth_response_hash)
|
||||
+ [RecordingStarted](#recordingstarted)
|
||||
+ [RecordingStopping](#recordingstopping)
|
||||
+ [RecordingStopped](#recordingstopped)
|
||||
+ [RecordingPaused](#recordingpaused)
|
||||
+ [RecordingResumed](#recordingresumed)
|
||||
* [Replay Buffer](#replay-buffer)
|
||||
+ [ReplayStarting](#replaystarting)
|
||||
+ [ReplayStarted](#replaystarted)
|
||||
@ -81,6 +84,7 @@ auth_response = base64_encode(auth_response_hash)
|
||||
+ [Exiting](#exiting)
|
||||
* [General](#general)
|
||||
+ [Heartbeat](#heartbeat)
|
||||
+ [BroadcastCustomMessage](#broadcastcustommessage)
|
||||
* [Sources](#sources)
|
||||
+ [SourceCreated](#sourcecreated)
|
||||
+ [SourceDestroyed](#sourcedestroyed)
|
||||
@ -91,6 +95,7 @@ auth_response = base64_encode(auth_response_hash)
|
||||
+ [SourceRenamed](#sourcerenamed)
|
||||
+ [SourceFilterAdded](#sourcefilteradded)
|
||||
+ [SourceFilterRemoved](#sourcefilterremoved)
|
||||
+ [SourceFilterVisibilityChanged](#sourcefiltervisibilitychanged)
|
||||
+ [SourceFiltersReordered](#sourcefiltersreordered)
|
||||
+ [SourceOrderChanged](#sourceorderchanged)
|
||||
+ [SceneItemAdded](#sceneitemadded)
|
||||
@ -111,7 +116,13 @@ auth_response = base64_encode(auth_response_hash)
|
||||
+ [SetFilenameFormatting](#setfilenameformatting)
|
||||
+ [GetFilenameFormatting](#getfilenameformatting)
|
||||
+ [GetStats](#getstats)
|
||||
+ [BroadcastCustomMessage](#broadcastcustommessage-1)
|
||||
+ [GetVideoInfo](#getvideoinfo)
|
||||
* [Outputs](#outputs)
|
||||
+ [ListOutputs](#listoutputs)
|
||||
+ [GetOutputInfo](#getoutputinfo)
|
||||
+ [StartOutput](#startoutput)
|
||||
+ [StopOutput](#stopoutput)
|
||||
* [Profiles](#profiles-1)
|
||||
+ [SetCurrentProfile](#setcurrentprofile)
|
||||
+ [GetCurrentProfile](#getcurrentprofile)
|
||||
@ -120,6 +131,8 @@ auth_response = base64_encode(auth_response_hash)
|
||||
+ [StartStopRecording](#startstoprecording)
|
||||
+ [StartRecording](#startrecording)
|
||||
+ [StopRecording](#stoprecording)
|
||||
+ [PauseRecording](#pauserecording)
|
||||
+ [ResumeRecording](#resumerecording)
|
||||
+ [SetRecordingFolder](#setrecordingfolder)
|
||||
+ [GetRecordingFolder](#getrecordingfolder)
|
||||
* [Replay Buffer](#replay-buffer-1)
|
||||
@ -166,11 +179,13 @@ auth_response = base64_encode(auth_response_hash)
|
||||
+ [SetBrowserSourceProperties](#setbrowsersourceproperties)
|
||||
+ [GetSpecialSources](#getspecialsources)
|
||||
+ [GetSourceFilters](#getsourcefilters)
|
||||
+ [GetSourceFilterInfo](#getsourcefilterinfo)
|
||||
+ [AddFilterToSource](#addfiltertosource)
|
||||
+ [RemoveFilterFromSource](#removefilterfromsource)
|
||||
+ [ReorderSourceFilter](#reordersourcefilter)
|
||||
+ [MoveSourceFilter](#movesourcefilter)
|
||||
+ [SetSourceFilterSettings](#setsourcefiltersettings)
|
||||
+ [SetSourceFilterVisibility](#setsourcefiltervisibility)
|
||||
+ [TakeSourceScreenshot](#takesourcescreenshot)
|
||||
* [Streaming](#streaming-1)
|
||||
+ [GetStreamingStatus](#getstreamingstatus)
|
||||
@ -256,6 +271,27 @@ These are complex types, such as `Source` and `Scene`, which are used as argumen
|
||||
| `cpu-usage` | _double_ | Current CPU usage (percentage) |
|
||||
| `memory-usage` | _double_ | Current RAM usage (in megabytes) |
|
||||
| `free-disk-space` | _double_ | Free recording disk space (in megabytes) |
|
||||
## Output
|
||||
| Name | Type | Description |
|
||||
| ---- | :---: | ------------|
|
||||
| `name` | _String_ | Output name |
|
||||
| `type` | _String_ | Output type/kind |
|
||||
| `width` | _int_ | Video output width |
|
||||
| `height` | _int_ | Video output height |
|
||||
| `flags` | _Object_ | Output flags |
|
||||
| `flags.rawValue` | _int_ | Raw flags value |
|
||||
| `flags.audio` | _boolean_ | Output uses audio |
|
||||
| `flags.video` | _boolean_ | Output uses video |
|
||||
| `flags.encoded` | _boolean_ | Output is encoded |
|
||||
| `flags.multiTrack` | _boolean_ | Output uses several audio tracks |
|
||||
| `flags.service` | _boolean_ | Output uses a service |
|
||||
| `settings` | _Object_ | Output name |
|
||||
| `active` | _boolean_ | Output status (active or not) |
|
||||
| `reconnecting` | _boolean_ | Output reconnection status (reconnecting or not) |
|
||||
| `congestion` | _double_ | Output congestion |
|
||||
| `totalFrames` | _int_ | Number of frames sent |
|
||||
| `droppedFrames` | _int_ | Number of frames dropped |
|
||||
| `totalBytes` | _int_ | Total bytes sent |
|
||||
## Scene
|
||||
| Name | Type | Description |
|
||||
| ---- | :---: | ------------|
|
||||
@ -579,6 +615,32 @@ _No additional response items._
|
||||
|
||||
---
|
||||
|
||||
### RecordingPaused
|
||||
|
||||
|
||||
- Added in v4.7.0
|
||||
|
||||
Current recording paused
|
||||
|
||||
**Response Items:**
|
||||
|
||||
_No additional response items._
|
||||
|
||||
---
|
||||
|
||||
### RecordingResumed
|
||||
|
||||
|
||||
- Added in v4.7.0
|
||||
|
||||
Current recording resumed
|
||||
|
||||
**Response Items:**
|
||||
|
||||
_No additional response items._
|
||||
|
||||
---
|
||||
|
||||
## Replay Buffer
|
||||
|
||||
### ReplayStarting
|
||||
@ -675,6 +737,23 @@ Emitted every 2 seconds after enabling it by calling SetHeartbeat.
|
||||
| `stats` | _OBSStats_ | OBS Stats |
|
||||
|
||||
|
||||
---
|
||||
|
||||
### BroadcastCustomMessage
|
||||
|
||||
|
||||
- Added in v4.7.0
|
||||
|
||||
A custom broadcast message was received
|
||||
|
||||
**Response Items:**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | :---: | ------------|
|
||||
| `realm` | _String_ | Identifier provided by the sender |
|
||||
| `data` | _Object_ | User-defined data |
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
@ -779,9 +858,9 @@ Audio mixer routing changed on a source.
|
||||
| Name | Type | Description |
|
||||
| ---- | :---: | ------------|
|
||||
| `sourceName` | _String_ | Source name |
|
||||
| `routingStatus` | _Array<Object>_ | Routing status of the source for each audio mixer (array of 6 values) |
|
||||
| `routingStatus.*.id` | _int_ | Mixer number |
|
||||
| `routingStatus.*.enabled` | _boolean_ | Routing status |
|
||||
| `mixers` | _Array<Object>_ | Routing status of the source for each audio mixer (array of 6 values) |
|
||||
| `mixers.*.id` | _int_ | Mixer number |
|
||||
| `mixers.*.enabled` | _boolean_ | Routing status |
|
||||
| `hexMixersValue` | _String_ | Raw mixer flags (little-endian, one bit per mixer) as an hexadecimal value |
|
||||
|
||||
|
||||
@ -839,6 +918,24 @@ A filter was removed from a source.
|
||||
| `filterType` | _String_ | Filter type |
|
||||
|
||||
|
||||
---
|
||||
|
||||
### SourceFilterVisibilityChanged
|
||||
|
||||
|
||||
- Added in v4.7.0
|
||||
|
||||
The visibility/enabled state of a filter changed
|
||||
|
||||
**Response Items:**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | :---: | ------------|
|
||||
| `sourceName` | _String_ | Source name |
|
||||
| `filterName` | _String_ | Filter name |
|
||||
| `filterEnabled` | _Boolean_ | New filter state |
|
||||
|
||||
|
||||
---
|
||||
|
||||
### SourceFiltersReordered
|
||||
@ -1186,6 +1283,27 @@ _No specified parameters._
|
||||
| `stats` | _OBSStats_ | OBS stats |
|
||||
|
||||
|
||||
---
|
||||
|
||||
### BroadcastCustomMessage
|
||||
|
||||
|
||||
- Added in v4.7.0
|
||||
|
||||
Broadcast custom message to all connected WebSocket clients
|
||||
|
||||
**Request Fields:**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | :---: | ------------|
|
||||
| `realm` | _String_ | Identifier to be choosen by the client |
|
||||
| `data` | _Object_ | User-defined data |
|
||||
|
||||
|
||||
**Response Items:**
|
||||
|
||||
_No additional response items._
|
||||
|
||||
---
|
||||
|
||||
### GetVideoInfo
|
||||
@ -1214,6 +1332,92 @@ _No specified parameters._
|
||||
| `colorRange` | _String_ | Color range (full or partial) |
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Outputs
|
||||
|
||||
### ListOutputs
|
||||
|
||||
|
||||
- Added in v4.7.0
|
||||
|
||||
List existing outputs
|
||||
|
||||
**Request Fields:**
|
||||
|
||||
_No specified parameters._
|
||||
|
||||
**Response Items:**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | :---: | ------------|
|
||||
| `outputs` | _Array<Output>_ | Outputs list |
|
||||
|
||||
|
||||
---
|
||||
|
||||
### GetOutputInfo
|
||||
|
||||
|
||||
- Added in v4.7.0
|
||||
|
||||
Get information about a single output
|
||||
|
||||
**Request Fields:**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | :---: | ------------|
|
||||
| `outputName` | _String_ | Output name |
|
||||
|
||||
|
||||
**Response Items:**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | :---: | ------------|
|
||||
| `outputInfo` | _Output_ | Output info |
|
||||
|
||||
|
||||
---
|
||||
|
||||
### StartOutput
|
||||
|
||||
|
||||
- Added in v4.7.0
|
||||
|
||||
Start an output
|
||||
|
||||
**Request Fields:**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | :---: | ------------|
|
||||
| `outputName` | _String_ | Output name |
|
||||
|
||||
|
||||
**Response Items:**
|
||||
|
||||
_No additional response items._
|
||||
|
||||
---
|
||||
|
||||
### StopOutput
|
||||
|
||||
|
||||
- Added in v4.7.0
|
||||
|
||||
Stop an output
|
||||
|
||||
**Request Fields:**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | :---: | ------------|
|
||||
| `outputName` | _String_ | Output name |
|
||||
| `force` | _boolean (optional)_ | Force stop (default: false) |
|
||||
|
||||
|
||||
**Response Items:**
|
||||
|
||||
_No additional response items._
|
||||
|
||||
---
|
||||
|
||||
## Profiles
|
||||
@ -1333,6 +1537,42 @@ _No additional response items._
|
||||
|
||||
---
|
||||
|
||||
### PauseRecording
|
||||
|
||||
|
||||
- Added in v4.7.0
|
||||
|
||||
Pause the current recording.
|
||||
Returns an error if recording is not active or already paused.
|
||||
|
||||
**Request Fields:**
|
||||
|
||||
_No specified parameters._
|
||||
|
||||
**Response Items:**
|
||||
|
||||
_No additional response items._
|
||||
|
||||
---
|
||||
|
||||
### ResumeRecording
|
||||
|
||||
|
||||
- Added in v4.7.0
|
||||
|
||||
Resume/unpause the current recording (if paused).
|
||||
Returns an error if recording is not active or not paused.
|
||||
|
||||
**Request Fields:**
|
||||
|
||||
_No specified parameters._
|
||||
|
||||
**Response Items:**
|
||||
|
||||
_No additional response items._
|
||||
|
||||
---
|
||||
|
||||
### SetRecordingFolder
|
||||
|
||||
|
||||
@ -1900,19 +2140,19 @@ _No specified parameters._
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | :---: | ------------|
|
||||
| `ids` | _Array<Object>_ | Array of source types |
|
||||
| `ids.*.typeId` | _String_ | Non-unique internal source type ID |
|
||||
| `ids.*.displayName` | _String_ | Display name of the source type |
|
||||
| `ids.*.type` | _String_ | Type. Value is one of the following: "input", "filter", "transition" or "other" |
|
||||
| `ids.*.defaultSettings` | _Object_ | Default settings of this source type |
|
||||
| `ids.*.caps` | _Object_ | Source type capabilities |
|
||||
| `ids.*.caps.isAsync` | _Boolean_ | True if source of this type provide frames asynchronously |
|
||||
| `ids.*.caps.hasVideo` | _Boolean_ | True if sources of this type provide video |
|
||||
| `ids.*.caps.hasAudio` | _Boolean_ | True if sources of this type provide audio |
|
||||
| `ids.*.caps.canInteract` | _Boolean_ | True if interaction with this sources of this type is possible |
|
||||
| `ids.*.caps.isComposite` | _Boolean_ | True if sources of this type composite one or more sub-sources |
|
||||
| `ids.*.caps.doNotDuplicate` | _Boolean_ | True if sources of this type should not be fully duplicated |
|
||||
| `ids.*.caps.doNotSelfMonitor` | _Boolean_ | True if sources of this type may cause a feedback loop if it's audio is monitored and shouldn't be |
|
||||
| `types` | _Array<Object>_ | Array of source types |
|
||||
| `types.*.typeId` | _String_ | Non-unique internal source type ID |
|
||||
| `types.*.displayName` | _String_ | Display name of the source type |
|
||||
| `types.*.type` | _String_ | Type. Value is one of the following: "input", "filter", "transition" or "other" |
|
||||
| `types.*.defaultSettings` | _Object_ | Default settings of this source type |
|
||||
| `types.*.caps` | _Object_ | Source type capabilities |
|
||||
| `types.*.caps.isAsync` | _Boolean_ | True if source of this type provide frames asynchronously |
|
||||
| `types.*.caps.hasVideo` | _Boolean_ | True if sources of this type provide video |
|
||||
| `types.*.caps.hasAudio` | _Boolean_ | True if sources of this type provide audio |
|
||||
| `types.*.caps.canInteract` | _Boolean_ | True if interaction with this sources of this type is possible |
|
||||
| `types.*.caps.isComposite` | _Boolean_ | True if sources of this type composite one or more sub-sources |
|
||||
| `types.*.caps.doNotDuplicate` | _Boolean_ | True if sources of this type should not be fully duplicated |
|
||||
| `types.*.caps.doNotSelfMonitor` | _Boolean_ | True if sources of this type may cause a feedback loop if it's audio is monitored and shouldn't be |
|
||||
|
||||
|
||||
---
|
||||
@ -2400,11 +2640,39 @@ List filters applied to a source
|
||||
| Name | Type | Description |
|
||||
| ---- | :---: | ------------|
|
||||
| `filters` | _Array<Object>_ | List of filters for the specified source |
|
||||
| `filters.*.enabled` | _Boolean_ | Filter status (enabled or not) |
|
||||
| `filters.*.type` | _String_ | Filter type |
|
||||
| `filters.*.name` | _String_ | Filter name |
|
||||
| `filters.*.settings` | _Object_ | Filter settings |
|
||||
|
||||
|
||||
---
|
||||
|
||||
### GetSourceFilterInfo
|
||||
|
||||
|
||||
- Added in v4.7.0
|
||||
|
||||
List filters applied to a source
|
||||
|
||||
**Request Fields:**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | :---: | ------------|
|
||||
| `sourceName` | _String_ | Source name |
|
||||
| `filterName` | _String_ | Source filter name |
|
||||
|
||||
|
||||
**Response Items:**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | :---: | ------------|
|
||||
| `enabled` | _Boolean_ | Filter status (enabled or not) |
|
||||
| `type` | _String_ | Filter type |
|
||||
| `name` | _String_ | Filter name |
|
||||
| `settings` | _Object_ | Filter settings |
|
||||
|
||||
|
||||
---
|
||||
|
||||
### AddFilterToSource
|
||||
@ -2511,6 +2779,28 @@ Update settings of a filter
|
||||
| `filterSettings` | _Object_ | New settings. These will be merged to the current filter settings. |
|
||||
|
||||
|
||||
**Response Items:**
|
||||
|
||||
_No additional response items._
|
||||
|
||||
---
|
||||
|
||||
### SetSourceFilterVisibility
|
||||
|
||||
|
||||
- Added in v4.7.0
|
||||
|
||||
Change the visibility/enabled state of a filter
|
||||
|
||||
**Request Fields:**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | :---: | ------------|
|
||||
| `sourceName` | _String_ | Source name |
|
||||
| `filterName` | _String_ | Source filter name |
|
||||
| `filterEnabled` | _String_ | New filter state |
|
||||
|
||||
|
||||
**Response Items:**
|
||||
|
||||
_No additional response items._
|
||||
@ -2533,7 +2823,7 @@ preserved if only one of these two parameters is specified.
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | :---: | ------------|
|
||||
| `sourceName` | _String_ | Source name |
|
||||
| `sourceName` | _String_ | Source name. Note that, since scenes are also sources, you can also provide a scene name. |
|
||||
| `embedPictureFormat` | _String (optional)_ | Format of the Data URI encoded picture. Can be "png", "jpg", "jpeg" or "bmp" (or any other value supported by Qt's Image module) |
|
||||
| `saveToFilePath` | _String (optional)_ | Full file path (file extension included) where the captured image is to be saved. Can be in a format different from `pictureFormat`. Can be a relative path. |
|
||||
| `width` | _int (optional)_ | Screenshot width. Defaults to the source's base width. |
|
||||
|
@ -1,4 +1,4 @@
|
||||
# obs-websocket 4.6.0 protocol reference
|
||||
# obs-websocket 4.7.0 protocol reference
|
||||
|
||||
# General Introduction
|
||||
Messages are exchanged between the client and the server as JSON objects.
|
||||
|
@ -2,7 +2,7 @@
|
||||
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
|
||||
|
||||
#define MyAppName "obs-websocket"
|
||||
#define MyAppVersion "4.6.1"
|
||||
#define MyAppVersion "4.7.0"
|
||||
#define MyAppPublisher "Stephane Lepin"
|
||||
#define MyAppURL "http://github.com/Palakis/obs-websocket"
|
||||
|
||||
|
217
src/Utils.cpp
217
src/Utils.cpp
@ -22,6 +22,8 @@ with this program. If not, see <https://www.gnu.org/licenses/>
|
||||
|
||||
#include <obs-frontend-api.h>
|
||||
#include <obs.hpp>
|
||||
#include <util/platform.h>
|
||||
|
||||
#include "obs-websocket.h"
|
||||
|
||||
#include "Utils.h"
|
||||
@ -173,23 +175,39 @@ obs_data_t* Utils::GetSceneItemData(obs_sceneitem_t* item) {
|
||||
return data;
|
||||
}
|
||||
|
||||
obs_sceneitem_t* Utils::GetSceneItemFromItem(obs_source_t* source, obs_data_t* item) {
|
||||
OBSSceneItem sceneItem;
|
||||
if (obs_data_has_user_value(item, "id")) {
|
||||
sceneItem = GetSceneItemFromId(source, obs_data_get_int(item, "id"));
|
||||
if (obs_data_has_user_value(item, "name") &&
|
||||
(QString)obs_source_get_name(obs_sceneitem_get_source(sceneItem)) !=
|
||||
(QString)obs_data_get_string(item, "name")) {
|
||||
obs_sceneitem_t* Utils::GetSceneItemFromItem(obs_scene_t* scene, obs_data_t* itemInfo) {
|
||||
if (!scene) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
else if (obs_data_has_user_value(item, "name")) {
|
||||
sceneItem = GetSceneItemFromName(source, obs_data_get_string(item, "name"));
|
||||
}
|
||||
return sceneItem;
|
||||
|
||||
OBSDataItemAutoRelease idInfoItem = obs_data_item_byname(itemInfo, "id");
|
||||
int id = obs_data_item_get_int(idInfoItem);
|
||||
|
||||
OBSDataItemAutoRelease nameInfoItem = obs_data_item_byname(itemInfo, "name");
|
||||
const char* name = obs_data_item_get_string(nameInfoItem);
|
||||
|
||||
if (idInfoItem) {
|
||||
obs_sceneitem_t* sceneItem = GetSceneItemFromId(scene, id);
|
||||
obs_source_t* sceneItemSource = obs_sceneitem_get_source(sceneItem);
|
||||
|
||||
QString sceneItemName = obs_source_get_name(sceneItemSource);
|
||||
if (nameInfoItem && (QString(name) != sceneItemName)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return sceneItem;
|
||||
} else if (nameInfoItem) {
|
||||
return GetSceneItemFromName(scene, name);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
obs_sceneitem_t* Utils::GetSceneItemFromName(obs_scene_t* scene, QString name) {
|
||||
if (!scene) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
obs_sceneitem_t* Utils::GetSceneItemFromName(obs_source_t* source, QString name) {
|
||||
struct current_search {
|
||||
QString query;
|
||||
obs_sceneitem_t* result;
|
||||
@ -199,11 +217,6 @@ obs_sceneitem_t* Utils::GetSceneItemFromName(obs_source_t* source, QString name)
|
||||
current_search search;
|
||||
search.query = name;
|
||||
search.result = nullptr;
|
||||
search.enumCallback = nullptr;
|
||||
|
||||
OBSScene scene = obs_scene_from_source(source);
|
||||
if (!scene)
|
||||
return nullptr;
|
||||
|
||||
search.enumCallback = [](
|
||||
obs_scene_t* scene,
|
||||
@ -236,10 +249,13 @@ obs_sceneitem_t* Utils::GetSceneItemFromName(obs_source_t* source, QString name)
|
||||
return search.result;
|
||||
}
|
||||
|
||||
// TODO refactor this to unify it with GetSceneItemFromName
|
||||
obs_sceneitem_t* Utils::GetSceneItemFromId(obs_source_t* source, size_t id) {
|
||||
obs_sceneitem_t* Utils::GetSceneItemFromId(obs_scene_t* scene, int64_t id) {
|
||||
if (!scene) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
struct current_search {
|
||||
size_t query;
|
||||
int query;
|
||||
obs_sceneitem_t* result;
|
||||
bool (*enumCallback)(obs_scene_t*, obs_sceneitem_t*, void*);
|
||||
};
|
||||
@ -247,11 +263,6 @@ obs_sceneitem_t* Utils::GetSceneItemFromId(obs_source_t* source, size_t id) {
|
||||
current_search search;
|
||||
search.query = id;
|
||||
search.result = nullptr;
|
||||
search.enumCallback = nullptr;
|
||||
|
||||
OBSScene scene = obs_scene_from_source(source);
|
||||
if (!scene)
|
||||
return nullptr;
|
||||
|
||||
search.enumCallback = [](
|
||||
obs_scene_t* scene,
|
||||
@ -261,7 +272,7 @@ obs_sceneitem_t* Utils::GetSceneItemFromId(obs_source_t* source, size_t id) {
|
||||
current_search* search = reinterpret_cast<current_search*>(param);
|
||||
|
||||
if (obs_sceneitem_is_group(currentItem)) {
|
||||
obs_sceneitem_group_enum_items(currentItem, search->enumCallback, param);
|
||||
obs_sceneitem_group_enum_items(currentItem, search->enumCallback, search);
|
||||
if (search->result) {
|
||||
return false;
|
||||
}
|
||||
@ -319,17 +330,19 @@ obs_source_t* Utils::GetTransitionFromName(QString searchName) {
|
||||
return foundTransition;
|
||||
}
|
||||
|
||||
obs_source_t* Utils::GetSceneFromNameOrCurrent(QString sceneName) {
|
||||
obs_scene_t* Utils::GetSceneFromNameOrCurrent(QString sceneName) {
|
||||
// Both obs_frontend_get_current_scene() and obs_get_source_by_name()
|
||||
// do addref on the return source, so no need to use an OBSSource helper
|
||||
obs_source_t* scene = nullptr;
|
||||
// increase the returned source's refcount
|
||||
OBSSourceAutoRelease sceneSource = nullptr;
|
||||
|
||||
if (sceneName.isEmpty() || sceneName.isNull())
|
||||
scene = obs_frontend_get_current_scene();
|
||||
else
|
||||
scene = obs_get_source_by_name(sceneName.toUtf8());
|
||||
if (sceneName.isEmpty() || sceneName.isNull()) {
|
||||
sceneSource = obs_frontend_get_current_scene();
|
||||
}
|
||||
else {
|
||||
sceneSource = obs_get_source_by_name(sceneName.toUtf8());
|
||||
}
|
||||
|
||||
return scene;
|
||||
return obs_scene_from_source(sceneSource);
|
||||
}
|
||||
|
||||
obs_data_array_t* Utils::GetScenes() {
|
||||
@ -362,18 +375,30 @@ QSpinBox* Utils::GetTransitionDurationControl() {
|
||||
return window->findChild<QSpinBox*>("transitionDuration");
|
||||
}
|
||||
|
||||
int Utils::GetTransitionDuration() {
|
||||
QSpinBox* control = GetTransitionDurationControl();
|
||||
if (control)
|
||||
return control->value();
|
||||
else
|
||||
int Utils::GetTransitionDuration(obs_source_t* transition) {
|
||||
if (!transition || obs_source_get_type(transition) != OBS_SOURCE_TYPE_TRANSITION) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
void Utils::SetTransitionDuration(int ms) {
|
||||
QSpinBox* control = GetTransitionDurationControl();
|
||||
if (control && ms >= 0)
|
||||
control->setValue(ms);
|
||||
QString transitionKind = obs_source_get_id(transition);
|
||||
if (transitionKind == "cut_transition") {
|
||||
// If this is a Cut transition, return 0
|
||||
return 0;
|
||||
}
|
||||
|
||||
OBSSourceAutoRelease destinationScene = obs_transition_get_active_source(transition);
|
||||
OBSDataAutoRelease destinationSettings = obs_source_get_private_settings(destinationScene);
|
||||
|
||||
// Detect if transition is the global transition or a transition override.
|
||||
// Fetching the duration is different depending on the case.
|
||||
obs_data_item_t* transitionDurationItem = obs_data_item_byname(destinationSettings, "transition_duration");
|
||||
int duration = (
|
||||
transitionDurationItem
|
||||
? obs_data_item_get_int(transitionDurationItem)
|
||||
: obs_frontend_get_transition_duration()
|
||||
);
|
||||
|
||||
return duration;
|
||||
}
|
||||
|
||||
bool Utils::SetTransitionByName(QString transitionName) {
|
||||
@ -387,40 +412,6 @@ bool Utils::SetTransitionByName(QString transitionName) {
|
||||
}
|
||||
}
|
||||
|
||||
QPushButton* Utils::GetPreviewModeButtonControl() {
|
||||
QMainWindow* main = (QMainWindow*)obs_frontend_get_main_window();
|
||||
return main->findChild<QPushButton*>("modeSwitch");
|
||||
}
|
||||
|
||||
QLayout* Utils::GetPreviewLayout() {
|
||||
QMainWindow* main = (QMainWindow*)obs_frontend_get_main_window();
|
||||
return main->findChild<QLayout*>("previewLayout");
|
||||
}
|
||||
|
||||
void Utils::TransitionToProgram() {
|
||||
if (!obs_frontend_preview_program_mode_active())
|
||||
return;
|
||||
|
||||
// WARNING : if the layout created in OBS' CreateProgramOptions() changes
|
||||
// then this won't work as expected
|
||||
|
||||
QMainWindow* main = (QMainWindow*)obs_frontend_get_main_window();
|
||||
|
||||
// The program options widget is the second item in the left-to-right layout
|
||||
QWidget* programOptions = GetPreviewLayout()->itemAt(1)->widget();
|
||||
|
||||
// The "Transition" button lies in the mainButtonLayout
|
||||
// which is the first itemin the program options' layout
|
||||
QLayout* mainButtonLayout = programOptions->layout()->itemAt(1)->layout();
|
||||
QWidget* transitionBtnWidget = mainButtonLayout->itemAt(0)->widget();
|
||||
|
||||
// Try to cast that widget into a button
|
||||
QPushButton* transitionBtn = qobject_cast<QPushButton*>(transitionBtnWidget);
|
||||
|
||||
// Perform a click on that button
|
||||
transitionBtn->click();
|
||||
}
|
||||
|
||||
QString Utils::OBSVersionString() {
|
||||
uint32_t version = obs_get_version();
|
||||
|
||||
@ -744,6 +735,19 @@ obs_data_t* Utils::GetSceneItemPropertiesData(obs_sceneitem_t* sceneItem) {
|
||||
return data;
|
||||
}
|
||||
|
||||
obs_data_t* Utils::GetSourceFilterInfo(obs_source_t* filter, bool includeSettings)
|
||||
{
|
||||
obs_data_t* data = obs_data_create();
|
||||
obs_data_set_bool(data, "enabled", obs_source_enabled(filter));
|
||||
obs_data_set_string(data, "type", obs_source_get_id(filter));
|
||||
obs_data_set_string(data, "name", obs_source_get_name(filter));
|
||||
if (includeSettings) {
|
||||
OBSDataAutoRelease settings = obs_source_get_settings(filter);
|
||||
obs_data_set_obj(data, "settings", settings);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
obs_data_array_t* Utils::GetSourceFiltersList(obs_source_t* source, bool includeSettings)
|
||||
{
|
||||
struct enum_params {
|
||||
@ -764,14 +768,57 @@ obs_data_array_t* Utils::GetSourceFiltersList(obs_source_t* source, bool include
|
||||
{
|
||||
auto enumParams = reinterpret_cast<struct enum_params*>(param);
|
||||
|
||||
OBSDataAutoRelease filter = obs_data_create();
|
||||
obs_data_set_string(filter, "type", obs_source_get_id(child));
|
||||
obs_data_set_string(filter, "name", obs_source_get_name(child));
|
||||
if (enumParams->includeSettings) {
|
||||
obs_data_set_obj(filter, "settings", obs_source_get_settings(child));
|
||||
}
|
||||
obs_data_array_push_back(enumParams->filters, filter);
|
||||
OBSDataAutoRelease filterData = Utils::GetSourceFilterInfo(child, enumParams->includeSettings);
|
||||
obs_data_array_push_back(enumParams->filters, filterData);
|
||||
}, &enumParams);
|
||||
|
||||
return enumParams.filters;
|
||||
}
|
||||
|
||||
void getPauseRecordingFunctions(RecordingPausedFunction* recPausedFuncPtr, PauseRecordingFunction* pauseRecFuncPtr)
|
||||
{
|
||||
void* frontendApi = os_dlopen("obs-frontend-api");
|
||||
|
||||
if (recPausedFuncPtr) {
|
||||
*recPausedFuncPtr = (RecordingPausedFunction)os_dlsym(frontendApi, "obs_frontend_recording_paused");
|
||||
}
|
||||
|
||||
if (pauseRecFuncPtr) {
|
||||
*pauseRecFuncPtr = (PauseRecordingFunction)os_dlsym(frontendApi, "obs_frontend_recording_pause");
|
||||
}
|
||||
|
||||
os_dlclose(frontendApi);
|
||||
}
|
||||
|
||||
bool Utils::RecordingPauseSupported()
|
||||
{
|
||||
RecordingPausedFunction recordingPaused = nullptr;
|
||||
PauseRecordingFunction pauseRecording = nullptr;
|
||||
getPauseRecordingFunctions(&recordingPaused, &pauseRecording);
|
||||
|
||||
return (recordingPaused && pauseRecording);
|
||||
}
|
||||
|
||||
bool Utils::RecordingPaused()
|
||||
{
|
||||
RecordingPausedFunction recordingPaused = nullptr;
|
||||
getPauseRecordingFunctions(&recordingPaused, nullptr);
|
||||
|
||||
if (recordingPaused == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return recordingPaused();
|
||||
}
|
||||
|
||||
void Utils::PauseRecording(bool pause)
|
||||
{
|
||||
PauseRecordingFunction pauseRecording = nullptr;
|
||||
getPauseRecordingFunctions(nullptr, &pauseRecording);
|
||||
|
||||
if (pauseRecording == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
pauseRecording(pause);
|
||||
}
|
||||
|
34
src/Utils.h
34
src/Utils.h
@ -31,19 +31,24 @@ with this program. If not, see <https://www.gnu.org/licenses/>
|
||||
#include <obs-module.h>
|
||||
#include <util/config-file.h>
|
||||
|
||||
typedef void(*PauseRecordingFunction)(bool);
|
||||
typedef bool(*RecordingPausedFunction)();
|
||||
|
||||
class Utils {
|
||||
public:
|
||||
static obs_data_array_t* StringListToArray(char** strings, const char* key);
|
||||
static obs_data_array_t* GetSceneItems(obs_source_t* source);
|
||||
static obs_data_t* GetSceneItemData(obs_sceneitem_t* item);
|
||||
static obs_sceneitem_t* GetSceneItemFromName(
|
||||
obs_source_t* source, QString name);
|
||||
static obs_sceneitem_t* GetSceneItemFromId(obs_source_t* source, size_t id);
|
||||
static obs_sceneitem_t* GetSceneItemFromItem(obs_source_t* source, obs_data_t* item);
|
||||
static obs_source_t* GetTransitionFromName(QString transitionName);
|
||||
static obs_source_t* GetSceneFromNameOrCurrent(QString sceneName);
|
||||
|
||||
// These two functions support nested lookup into groups
|
||||
static obs_sceneitem_t* GetSceneItemFromName(obs_scene_t* scene, QString name);
|
||||
static obs_sceneitem_t* GetSceneItemFromId(obs_scene_t* scene, int64_t id);
|
||||
|
||||
static obs_sceneitem_t* GetSceneItemFromItem(obs_scene_t* scene, obs_data_t* item);
|
||||
static obs_scene_t* GetSceneFromNameOrCurrent(QString sceneName);
|
||||
static obs_data_t* GetSceneItemPropertiesData(obs_sceneitem_t* item);
|
||||
|
||||
static obs_data_t* GetSourceFilterInfo(obs_source_t* filter, bool includeSettings);
|
||||
static obs_data_array_t* GetSourceFiltersList(obs_source_t* source, bool includeSettings);
|
||||
|
||||
static bool IsValidAlignment(const uint32_t alignment);
|
||||
@ -53,17 +58,10 @@ class Utils {
|
||||
|
||||
// TODO contribute a proper frontend API method for this to OBS and remove this hack
|
||||
static QSpinBox* GetTransitionDurationControl();
|
||||
static int GetTransitionDuration();
|
||||
static void SetTransitionDuration(int ms);
|
||||
|
||||
static int GetTransitionDuration(obs_source_t* transition);
|
||||
static obs_source_t* GetTransitionFromName(QString transitionName);
|
||||
static bool SetTransitionByName(QString transitionName);
|
||||
|
||||
static QPushButton* GetPreviewModeButtonControl();
|
||||
static QLayout* GetPreviewLayout();
|
||||
|
||||
// TODO contribute a proper frontend API method for this to OBS and remove this hack
|
||||
static void TransitionToProgram();
|
||||
|
||||
static QString OBSVersionString();
|
||||
|
||||
static QSystemTrayIcon* GetTrayIcon();
|
||||
@ -77,9 +75,15 @@ class Utils {
|
||||
|
||||
static QString ParseDataToQueryString(obs_data_t* data);
|
||||
static obs_hotkey_t* FindHotkeyByName(QString name);
|
||||
|
||||
static bool ReplayBufferEnabled();
|
||||
static void StartReplayBuffer();
|
||||
static bool IsRPHotkeySet();
|
||||
|
||||
static const char* GetFilenameFormatting();
|
||||
static bool SetFilenameFormatting(const char* filenameFormatting);
|
||||
|
||||
static bool RecordingPauseSupported();
|
||||
static bool RecordingPaused();
|
||||
static void PauseRecording(bool pause);
|
||||
};
|
||||
|
372
src/WSEvents.cpp
372
src/WSEvents.cpp
@ -17,7 +17,9 @@
|
||||
* with this program. If not, see <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <util/platform.h>
|
||||
#include <media-io/video-io.h>
|
||||
|
||||
#include <QtWidgets/QPushButton>
|
||||
|
||||
@ -29,31 +31,17 @@
|
||||
|
||||
#define STATUS_INTERVAL 2000
|
||||
|
||||
bool transitionIsCut(obs_source_t* transition) {
|
||||
if (!transition)
|
||||
return false;
|
||||
QString nsToTimestamp(uint64_t ns) {
|
||||
uint64_t ms = ns / 1000000ULL;
|
||||
uint64_t secs = ms / 1000ULL;
|
||||
uint64_t minutes = secs / 60ULL;
|
||||
|
||||
if (obs_source_get_type(transition) == OBS_SOURCE_TYPE_TRANSITION
|
||||
&& QString(obs_source_get_id(transition)) == "cut_transition") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
uint64_t hoursPart = minutes / 60ULL;
|
||||
uint64_t minutesPart = minutes % 60ULL;
|
||||
uint64_t secsPart = secs % 60ULL;
|
||||
uint64_t msPart = ms % 1000ULL;
|
||||
|
||||
const char* nsToTimestamp(uint64_t ns) {
|
||||
uint64_t ms = ns / (1000 * 1000);
|
||||
uint64_t secs = ms / 1000;
|
||||
uint64_t minutes = secs / 60;
|
||||
|
||||
uint64_t hoursPart = minutes / 60;
|
||||
uint64_t minutesPart = minutes % 60;
|
||||
uint64_t secsPart = secs % 60;
|
||||
uint64_t msPart = ms % 1000;
|
||||
|
||||
char* ts = (char*)bmalloc(64);
|
||||
sprintf(ts, "%02lu:%02lu:%02lu.%03lu", hoursPart, minutesPart, secsPart, msPart);
|
||||
|
||||
return ts;
|
||||
return QString::asprintf("%02" PRIu64 ":%02" PRIu64 ":%02" PRIu64 ".%03" PRIu64, hoursPart, minutesPart, secsPart, msPart);
|
||||
}
|
||||
|
||||
const char* sourceTypeToString(obs_source_type type) {
|
||||
@ -86,7 +74,8 @@ const char* calldata_get_string(const calldata_t* data, const char* name) {
|
||||
WSEvents::WSEvents(WSServerPtr srv) :
|
||||
_srv(srv),
|
||||
_streamStarttime(0),
|
||||
_recStarttime(0),
|
||||
_lastBytesSent(0),
|
||||
_lastBytesSentTime(0),
|
||||
HeartbeatIsActive(false),
|
||||
pulse(false)
|
||||
{
|
||||
@ -143,87 +132,120 @@ void WSEvents::FrontendEventHandler(enum obs_frontend_event event, void* private
|
||||
return;
|
||||
}
|
||||
|
||||
if (event == OBS_FRONTEND_EVENT_FINISHED_LOADING) {
|
||||
switch (event) {
|
||||
case OBS_FRONTEND_EVENT_FINISHED_LOADING:
|
||||
owner->hookTransitionBeginEvent();
|
||||
}
|
||||
else if (event == OBS_FRONTEND_EVENT_SCENE_CHANGED) {
|
||||
break;
|
||||
|
||||
case OBS_FRONTEND_EVENT_SCENE_CHANGED:
|
||||
owner->OnSceneChange();
|
||||
}
|
||||
else if (event == OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED) {
|
||||
break;
|
||||
|
||||
case OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED:
|
||||
owner->OnSceneListChange();
|
||||
}
|
||||
else if (event == OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED) {
|
||||
break;
|
||||
|
||||
case OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED:
|
||||
owner->hookTransitionBeginEvent();
|
||||
owner->OnSceneCollectionChange();
|
||||
}
|
||||
else if (event == OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED) {
|
||||
break;
|
||||
|
||||
case OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED:
|
||||
owner->OnSceneCollectionListChange();
|
||||
}
|
||||
else if (event == OBS_FRONTEND_EVENT_TRANSITION_CHANGED) {
|
||||
break;
|
||||
|
||||
case OBS_FRONTEND_EVENT_TRANSITION_CHANGED:
|
||||
owner->OnTransitionChange();
|
||||
}
|
||||
else if (event == OBS_FRONTEND_EVENT_TRANSITION_LIST_CHANGED) {
|
||||
break;
|
||||
|
||||
case OBS_FRONTEND_EVENT_TRANSITION_LIST_CHANGED:
|
||||
owner->hookTransitionBeginEvent();
|
||||
owner->OnTransitionListChange();
|
||||
}
|
||||
else if (event == OBS_FRONTEND_EVENT_PROFILE_CHANGED) {
|
||||
break;
|
||||
|
||||
case OBS_FRONTEND_EVENT_PROFILE_CHANGED:
|
||||
owner->OnProfileChange();
|
||||
}
|
||||
else if (event == OBS_FRONTEND_EVENT_PROFILE_LIST_CHANGED) {
|
||||
break;
|
||||
|
||||
case OBS_FRONTEND_EVENT_PROFILE_LIST_CHANGED:
|
||||
owner->OnProfileListChange();
|
||||
}
|
||||
else if (event == OBS_FRONTEND_EVENT_STREAMING_STARTING) {
|
||||
break;
|
||||
|
||||
case OBS_FRONTEND_EVENT_STREAMING_STARTING:
|
||||
owner->OnStreamStarting();
|
||||
}
|
||||
else if (event == OBS_FRONTEND_EVENT_STREAMING_STARTED) {
|
||||
break;
|
||||
|
||||
case OBS_FRONTEND_EVENT_STREAMING_STARTED:
|
||||
owner->streamStatusTimer.start(STATUS_INTERVAL);
|
||||
owner->StreamStatus();
|
||||
|
||||
owner->OnStreamStarted();
|
||||
}
|
||||
else if (event == OBS_FRONTEND_EVENT_STREAMING_STOPPING) {
|
||||
break;
|
||||
|
||||
case OBS_FRONTEND_EVENT_STREAMING_STOPPING:
|
||||
owner->streamStatusTimer.stop();
|
||||
owner->OnStreamStopping();
|
||||
}
|
||||
else if (event == OBS_FRONTEND_EVENT_STREAMING_STOPPED) {
|
||||
break;
|
||||
|
||||
case OBS_FRONTEND_EVENT_STREAMING_STOPPED:
|
||||
owner->OnStreamStopped();
|
||||
}
|
||||
else if (event == OBS_FRONTEND_EVENT_RECORDING_STARTING) {
|
||||
break;
|
||||
|
||||
case OBS_FRONTEND_EVENT_RECORDING_STARTING:
|
||||
owner->OnRecordingStarting();
|
||||
}
|
||||
else if (event == OBS_FRONTEND_EVENT_RECORDING_STARTED) {
|
||||
break;
|
||||
|
||||
case OBS_FRONTEND_EVENT_RECORDING_STARTED:
|
||||
owner->OnRecordingStarted();
|
||||
}
|
||||
else if (event == OBS_FRONTEND_EVENT_RECORDING_STOPPING) {
|
||||
break;
|
||||
|
||||
case OBS_FRONTEND_EVENT_RECORDING_STOPPING:
|
||||
owner->OnRecordingStopping();
|
||||
}
|
||||
else if (event == OBS_FRONTEND_EVENT_RECORDING_STOPPED) {
|
||||
break;
|
||||
|
||||
case OBS_FRONTEND_EVENT_RECORDING_STOPPED:
|
||||
owner->OnRecordingStopped();
|
||||
}
|
||||
else if (event == OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING) {
|
||||
break;
|
||||
|
||||
case OBS_FRONTEND_EVENT_RECORDING_PAUSED:
|
||||
owner->OnRecordingPaused();
|
||||
break;
|
||||
|
||||
case OBS_FRONTEND_EVENT_RECORDING_UNPAUSED:
|
||||
owner->OnRecordingResumed();
|
||||
break;
|
||||
|
||||
case OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING:
|
||||
owner->OnReplayStarting();
|
||||
}
|
||||
else if (event == OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED) {
|
||||
break;
|
||||
|
||||
case OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED:
|
||||
owner->OnReplayStarted();
|
||||
}
|
||||
else if (event == OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING) {
|
||||
break;
|
||||
|
||||
case OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING:
|
||||
owner->OnReplayStopping();
|
||||
}
|
||||
else if (event == OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED) {
|
||||
break;
|
||||
|
||||
case OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED:
|
||||
owner->OnReplayStopped();
|
||||
}
|
||||
else if (event == OBS_FRONTEND_EVENT_STUDIO_MODE_ENABLED) {
|
||||
break;
|
||||
|
||||
case OBS_FRONTEND_EVENT_STUDIO_MODE_ENABLED:
|
||||
owner->OnStudioModeSwitched(true);
|
||||
}
|
||||
else if (event == OBS_FRONTEND_EVENT_STUDIO_MODE_DISABLED) {
|
||||
break;
|
||||
|
||||
case OBS_FRONTEND_EVENT_STUDIO_MODE_DISABLED:
|
||||
owner->OnStudioModeSwitched(false);
|
||||
}
|
||||
else if (event == OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED) {
|
||||
break;
|
||||
|
||||
case OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED:
|
||||
owner->OnPreviewSceneChanged();
|
||||
}
|
||||
else if (event == OBS_FRONTEND_EVENT_EXIT) {
|
||||
break;
|
||||
|
||||
case OBS_FRONTEND_EVENT_EXIT:
|
||||
owner->unhookTransitionBeginEvent();
|
||||
owner->OnExit();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -233,17 +255,14 @@ void WSEvents::broadcastUpdate(const char* updateType,
|
||||
OBSDataAutoRelease update = obs_data_create();
|
||||
obs_data_set_string(update, "update-type", updateType);
|
||||
|
||||
const char* ts = nullptr;
|
||||
if (obs_frontend_streaming_active()) {
|
||||
ts = nsToTimestamp(os_gettime_ns() - _streamStarttime);
|
||||
obs_data_set_string(update, "stream-timecode", ts);
|
||||
bfree((void*)ts);
|
||||
QString streamingTimecode = getStreamingTimecode();
|
||||
obs_data_set_string(update, "stream-timecode", streamingTimecode.toUtf8().constData());
|
||||
}
|
||||
|
||||
if (obs_frontend_recording_active()) {
|
||||
ts = nsToTimestamp(os_gettime_ns() - _recStarttime);
|
||||
obs_data_set_string(update, "rec-timecode", ts);
|
||||
bfree((void*)ts);
|
||||
QString recordingTimecode = getRecordingTimecode();
|
||||
obs_data_set_string(update, "rec-timecode", recordingTimecode.toUtf8().constData());
|
||||
}
|
||||
|
||||
if (additionalFields)
|
||||
@ -321,6 +340,26 @@ void WSEvents::disconnectSourceSignals(obs_source_t* source) {
|
||||
signal_handler_disconnect(sh, "transition_start", OnTransitionBegin, this);
|
||||
}
|
||||
|
||||
void WSEvents::connectFilterSignals(obs_source_t* filter) {
|
||||
if (!filter) {
|
||||
return;
|
||||
}
|
||||
|
||||
signal_handler_t* sh = obs_source_get_signal_handler(filter);
|
||||
|
||||
signal_handler_connect(sh, "enable", OnSourceFilterVisibilityChanged, this);
|
||||
}
|
||||
|
||||
void WSEvents::disconnectFilterSignals(obs_source_t* filter) {
|
||||
if (!filter) {
|
||||
return;
|
||||
}
|
||||
|
||||
signal_handler_t* sh = obs_source_get_signal_handler(filter);
|
||||
|
||||
signal_handler_disconnect(sh, "enable", OnSourceFilterVisibilityChanged, this);
|
||||
}
|
||||
|
||||
void WSEvents::hookTransitionBeginEvent() {
|
||||
obs_frontend_source_list transitions = {};
|
||||
obs_frontend_get_transitions(&transitions);
|
||||
@ -348,26 +387,34 @@ void WSEvents::unhookTransitionBeginEvent() {
|
||||
obs_frontend_source_list_free(&transitions);
|
||||
}
|
||||
|
||||
uint64_t WSEvents::GetStreamingTime() {
|
||||
if (obs_frontend_streaming_active())
|
||||
return (os_gettime_ns() - _streamStarttime);
|
||||
else
|
||||
uint64_t getOutputRunningTime(obs_output_t* output) {
|
||||
if (!output || !obs_output_active(output)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char* WSEvents::GetStreamingTimecode() {
|
||||
return nsToTimestamp(GetStreamingTime());
|
||||
video_t* video = obs_output_video(output);
|
||||
uint64_t frameTimeNs = video_output_get_frame_time(video);
|
||||
int totalFrames = obs_output_get_total_frames(output);
|
||||
|
||||
return (((uint64_t)totalFrames) * frameTimeNs);
|
||||
}
|
||||
|
||||
uint64_t WSEvents::GetRecordingTime() {
|
||||
if (obs_frontend_recording_active())
|
||||
return (os_gettime_ns() - _recStarttime);
|
||||
else
|
||||
return 0;
|
||||
uint64_t WSEvents::getStreamingTime() {
|
||||
OBSOutputAutoRelease streamingOutput = obs_frontend_get_streaming_output();
|
||||
return getOutputRunningTime(streamingOutput);
|
||||
}
|
||||
|
||||
const char* WSEvents::GetRecordingTimecode() {
|
||||
return nsToTimestamp(GetRecordingTime());
|
||||
uint64_t WSEvents::getRecordingTime() {
|
||||
OBSOutputAutoRelease recordingOutput = obs_frontend_get_recording_output();
|
||||
return getOutputRunningTime(recordingOutput);
|
||||
}
|
||||
|
||||
QString WSEvents::getStreamingTimecode() {
|
||||
return nsToTimestamp(getStreamingTime());
|
||||
}
|
||||
|
||||
QString WSEvents::getRecordingTimecode() {
|
||||
return nsToTimestamp(getRecordingTime());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -520,6 +567,7 @@ void WSEvents::OnStreamStarting() {
|
||||
void WSEvents::OnStreamStarted() {
|
||||
_streamStarttime = os_gettime_ns();
|
||||
_lastBytesSent = 0;
|
||||
|
||||
broadcastUpdate("StreamStarted");
|
||||
}
|
||||
|
||||
@ -536,7 +584,6 @@ void WSEvents::OnStreamStarted() {
|
||||
void WSEvents::OnStreamStopping() {
|
||||
OBSDataAutoRelease data = obs_data_create();
|
||||
obs_data_set_bool(data, "preview-only", false);
|
||||
|
||||
broadcastUpdate("StreamStopping", data);
|
||||
}
|
||||
|
||||
@ -550,6 +597,7 @@ void WSEvents::OnStreamStopping() {
|
||||
*/
|
||||
void WSEvents::OnStreamStopped() {
|
||||
_streamStarttime = 0;
|
||||
|
||||
broadcastUpdate("StreamStopped");
|
||||
}
|
||||
|
||||
@ -574,7 +622,6 @@ void WSEvents::OnRecordingStarting() {
|
||||
* @since 0.3
|
||||
*/
|
||||
void WSEvents::OnRecordingStarted() {
|
||||
_recStarttime = os_gettime_ns();
|
||||
broadcastUpdate("RecordingStarted");
|
||||
}
|
||||
|
||||
@ -599,10 +646,33 @@ void WSEvents::OnRecordingStopping() {
|
||||
* @since 0.3
|
||||
*/
|
||||
void WSEvents::OnRecordingStopped() {
|
||||
_recStarttime = 0;
|
||||
broadcastUpdate("RecordingStopped");
|
||||
}
|
||||
|
||||
/**
|
||||
* Current recording paused
|
||||
*
|
||||
* @api events
|
||||
* @name RecordingPaused
|
||||
* @category recording
|
||||
* @since 4.7.0
|
||||
*/
|
||||
void WSEvents::OnRecordingPaused() {
|
||||
broadcastUpdate("RecordingPaused");
|
||||
}
|
||||
|
||||
/**
|
||||
* Current recording resumed
|
||||
*
|
||||
* @api events
|
||||
* @name RecordingResumed
|
||||
* @category recording
|
||||
* @since 4.7.0
|
||||
*/
|
||||
void WSEvents::OnRecordingResumed() {
|
||||
broadcastUpdate("RecordingResumed");
|
||||
}
|
||||
|
||||
/**
|
||||
* A request to start the replay buffer has been issued.
|
||||
*
|
||||
@ -694,6 +764,7 @@ void WSEvents::OnExit() {
|
||||
void WSEvents::StreamStatus() {
|
||||
bool streamingActive = obs_frontend_streaming_active();
|
||||
bool recordingActive = obs_frontend_recording_active();
|
||||
bool recordingPaused = Utils::RecordingPaused();
|
||||
bool replayBufferActive = obs_frontend_replay_buffer_active();
|
||||
|
||||
OBSOutputAutoRelease streamOutput = obs_frontend_get_streaming_output();
|
||||
@ -720,9 +791,7 @@ void WSEvents::StreamStatus() {
|
||||
_lastBytesSent = bytesSent;
|
||||
_lastBytesSentTime = bytesSentTime;
|
||||
|
||||
uint64_t totalStreamTime =
|
||||
(os_gettime_ns() - _streamStarttime) / 1000000000;
|
||||
|
||||
uint64_t totalStreamTime = (getStreamingTime() / 1000000000ULL);
|
||||
int totalFrames = obs_output_get_total_frames(streamOutput);
|
||||
int droppedFrames = obs_output_get_frames_dropped(streamOutput);
|
||||
|
||||
@ -732,6 +801,7 @@ void WSEvents::StreamStatus() {
|
||||
|
||||
obs_data_set_bool(data, "streaming", streamingActive);
|
||||
obs_data_set_bool(data, "recording", recordingActive);
|
||||
obs_data_set_bool(data, "recording-paused", recordingPaused);
|
||||
obs_data_set_bool(data, "replay-buffer-active", replayBufferActive);
|
||||
|
||||
obs_data_set_int(data, "bytes-per-sec", bytesPerSec);
|
||||
@ -778,6 +848,7 @@ void WSEvents::Heartbeat() {
|
||||
|
||||
bool streamingActive = obs_frontend_streaming_active();
|
||||
bool recordingActive = obs_frontend_recording_active();
|
||||
bool recordingPaused = Utils::RecordingPaused();
|
||||
|
||||
OBSDataAutoRelease data = obs_data_create();
|
||||
OBSOutputAutoRelease recordOutput = obs_frontend_get_recording_output();
|
||||
@ -793,16 +864,15 @@ void WSEvents::Heartbeat() {
|
||||
|
||||
obs_data_set_bool(data, "streaming", streamingActive);
|
||||
if (streamingActive) {
|
||||
uint64_t totalStreamTime = (os_gettime_ns() - _streamStarttime) / 1000000000;
|
||||
obs_data_set_int(data, "total-stream-time", totalStreamTime);
|
||||
obs_data_set_int(data, "total-stream-time", (getStreamingTime() / 1000000000ULL));
|
||||
obs_data_set_int(data, "total-stream-bytes", (uint64_t)obs_output_get_total_bytes(streamOutput));
|
||||
obs_data_set_int(data, "total-stream-frames", obs_output_get_total_frames(streamOutput));
|
||||
}
|
||||
|
||||
obs_data_set_bool(data, "recording", recordingActive);
|
||||
obs_data_set_bool(data, "recording-paused", recordingPaused);
|
||||
if (recordingActive) {
|
||||
uint64_t totalRecordTime = (os_gettime_ns() - _recStarttime) / 1000000000;
|
||||
obs_data_set_int(data, "total-record-time", totalRecordTime);
|
||||
obs_data_set_int(data, "total-record-time", (getRecordingTime() / 1000000000ULL));
|
||||
obs_data_set_int(data, "total-record-bytes", (uint64_t)obs_output_get_total_bytes(recordOutput));
|
||||
obs_data_set_int(data, "total-record-frames", obs_output_get_total_frames(recordOutput));
|
||||
}
|
||||
@ -847,33 +917,25 @@ void WSEvents::OnTransitionBegin(void* param, calldata_t* data) {
|
||||
auto instance = reinterpret_cast<WSEvents*>(param);
|
||||
|
||||
OBSSource transition = calldata_get_pointer<obs_source_t>(data, "source");
|
||||
if (!transition) return;
|
||||
if (!transition) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Detect if transition is the global transition or a transition override.
|
||||
// Fetching the duration is different depending on the case.
|
||||
OBSSourceAutoRelease sourceScene = obs_transition_get_source(transition, OBS_TRANSITION_SOURCE_A);
|
||||
OBSSourceAutoRelease destinationScene = obs_transition_get_active_source(transition);
|
||||
OBSDataAutoRelease destinationSettings = obs_source_get_private_settings(destinationScene);
|
||||
int duration = -1;
|
||||
if (obs_data_has_default_value(destinationSettings, "transition_duration") ||
|
||||
obs_data_has_user_value(destinationSettings, "transition_duration"))
|
||||
{
|
||||
duration = obs_data_get_int(destinationSettings, "transition_duration");
|
||||
} else {
|
||||
duration = Utils::GetTransitionDuration();
|
||||
int duration = Utils::GetTransitionDuration(transition);
|
||||
if (duration < 0) {
|
||||
blog(LOG_WARNING, "OnTransitionBegin: duration is negative !");
|
||||
}
|
||||
|
||||
OBSDataAutoRelease fields = obs_data_create();
|
||||
obs_data_set_string(fields, "name", obs_source_get_name(transition));
|
||||
if (duration >= 0) {
|
||||
obs_data_set_int(fields, "duration", duration);
|
||||
} else {
|
||||
blog(LOG_WARNING, "OnTransitionBegin: duration is negative !");
|
||||
}
|
||||
|
||||
OBSSourceAutoRelease sourceScene = obs_transition_get_source(transition, OBS_TRANSITION_SOURCE_A);
|
||||
if (sourceScene) {
|
||||
obs_data_set_string(fields, "from-scene", obs_source_get_name(sourceScene));
|
||||
}
|
||||
|
||||
OBSSourceAutoRelease destinationScene = obs_transition_get_active_source(transition);
|
||||
if (destinationScene) {
|
||||
obs_data_set_string(fields, "to-scene", obs_source_get_name(destinationScene));
|
||||
}
|
||||
@ -1041,9 +1103,9 @@ void WSEvents::OnSourceAudioSyncOffsetChanged(void* param, calldata_t* data) {
|
||||
* Audio mixer routing changed on a source.
|
||||
*
|
||||
* @return {String} `sourceName` Source name
|
||||
* @return {Array<Object>} `routingStatus` Routing status of the source for each audio mixer (array of 6 values)
|
||||
* @return {int} `routingStatus.*.id` Mixer number
|
||||
* @return {boolean} `routingStatus.*.enabled` Routing status
|
||||
* @return {Array<Object>} `mixers` Routing status of the source for each audio mixer (array of 6 values)
|
||||
* @return {int} `mixers.*.id` Mixer number
|
||||
* @return {boolean} `mixers.*.enabled` Routing status
|
||||
* @return {String} `hexMixersValue` Raw mixer flags (little-endian, one bit per mixer) as an hexadecimal value
|
||||
*
|
||||
* @api events
|
||||
@ -1139,6 +1201,8 @@ void WSEvents::OnSourceFilterAdded(void* param, calldata_t* data) {
|
||||
return;
|
||||
}
|
||||
|
||||
self->connectFilterSignals(filter);
|
||||
|
||||
OBSDataAutoRelease filterSettings = obs_source_get_settings(filter);
|
||||
|
||||
OBSDataAutoRelease fields = obs_data_create();
|
||||
@ -1170,6 +1234,11 @@ void WSEvents::OnSourceFilterRemoved(void* param, calldata_t* data) {
|
||||
}
|
||||
|
||||
obs_source_t* filter = calldata_get_pointer<obs_source_t>(data, "filter");
|
||||
if (!filter) {
|
||||
return;
|
||||
}
|
||||
|
||||
self->disconnectFilterSignals(filter);
|
||||
|
||||
OBSDataAutoRelease fields = obs_data_create();
|
||||
obs_data_set_string(fields, "sourceName", obs_source_get_name(source));
|
||||
@ -1178,6 +1247,36 @@ void WSEvents::OnSourceFilterRemoved(void* param, calldata_t* data) {
|
||||
self->broadcastUpdate("SourceFilterRemoved", fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* The visibility/enabled state of a filter changed
|
||||
*
|
||||
* @return {String} `sourceName` Source name
|
||||
* @return {String} `filterName` Filter name
|
||||
* @return {Boolean} `filterEnabled` New filter state
|
||||
*
|
||||
* @api events
|
||||
* @name SourceFilterVisibilityChanged
|
||||
* @category sources
|
||||
* @since 4.7.0
|
||||
*/
|
||||
void WSEvents::OnSourceFilterVisibilityChanged(void* param, calldata_t* data) {
|
||||
auto self = reinterpret_cast<WSEvents*>(param);
|
||||
|
||||
OBSSource source = calldata_get_pointer<obs_source_t>(data, "source");
|
||||
if (!source) {
|
||||
return;
|
||||
}
|
||||
|
||||
OBSSource parent = obs_filter_get_parent(source);
|
||||
|
||||
OBSDataAutoRelease fields = obs_data_create();
|
||||
obs_data_set_string(fields, "sourceName", obs_source_get_name(parent));
|
||||
obs_data_set_string(fields, "filterName", obs_source_get_name(source));
|
||||
obs_data_set_bool(fields, "filterEnabled", obs_source_enabled(source));
|
||||
|
||||
self->broadcastUpdate("SourceFilterVisibilityChanged", fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters in a source have been reordered.
|
||||
*
|
||||
@ -1505,6 +1604,25 @@ void WSEvents::OnStudioModeSwitched(bool checked) {
|
||||
broadcastUpdate("StudioModeSwitched", data);
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom broadcast message was received
|
||||
*
|
||||
* @return {String} `realm` Identifier provided by the sender
|
||||
* @return {Object} `data` User-defined data
|
||||
*
|
||||
* @api events
|
||||
* @name BroadcastCustomMessage
|
||||
* @category general
|
||||
* @since 4.7.0
|
||||
*/
|
||||
void WSEvents::OnBroadcastCustomMessage(QString realm, obs_data_t* data) {
|
||||
OBSDataAutoRelease broadcastData = obs_data_create();
|
||||
obs_data_set_string(broadcastData, "realm", realm.toUtf8().constData());
|
||||
obs_data_set_obj(broadcastData, "data", data);
|
||||
|
||||
broadcastUpdate("BroadcastCustomMessage", broadcastData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} `OBSStats`
|
||||
* @property {double} `fps` Current framerate.
|
||||
@ -1530,12 +1648,12 @@ obs_data_t* WSEvents::GetStats() {
|
||||
double averageFrameTime = (double)obs_get_average_frame_time_ns() / 1000000.0;
|
||||
|
||||
config_t* currentProfile = obs_frontend_get_profile_config();
|
||||
QString outputMode = config_get_string(currentProfile, "Output", "Mode");
|
||||
QString path = (outputMode == "Advanced") ?
|
||||
const char* outputMode = config_get_string(currentProfile, "Output", "Mode");
|
||||
const char* path = strcmp(outputMode, "Advanced") ?
|
||||
config_get_string(currentProfile, "SimpleOutput", "FilePath") :
|
||||
config_get_string(currentProfile, "AdvOut", "RecFilePath");
|
||||
|
||||
double freeDiskSpace = (double)os_get_free_disk_space(path.toUtf8()) / (1024.0 * 1024.0);
|
||||
double freeDiskSpace = (double)os_get_free_disk_space(path) / (1024.0 * 1024.0);
|
||||
|
||||
obs_data_set_double(stats, "fps", obs_get_active_fps());
|
||||
obs_data_set_int(stats, "render-total-frames", obs_get_total_frames());
|
||||
|
@ -40,15 +40,22 @@ public:
|
||||
void connectSourceSignals(obs_source_t* source);
|
||||
void disconnectSourceSignals(obs_source_t* source);
|
||||
|
||||
void connectFilterSignals(obs_source_t* filter);
|
||||
void disconnectFilterSignals(obs_source_t* filter);
|
||||
|
||||
void hookTransitionBeginEvent();
|
||||
void unhookTransitionBeginEvent();
|
||||
|
||||
uint64_t GetStreamingTime();
|
||||
const char* GetStreamingTimecode();
|
||||
uint64_t GetRecordingTime();
|
||||
const char* GetRecordingTimecode();
|
||||
uint64_t getStreamingTime();
|
||||
uint64_t getRecordingTime();
|
||||
|
||||
QString getStreamingTimecode();
|
||||
QString getRecordingTimecode();
|
||||
|
||||
obs_data_t* GetStats();
|
||||
|
||||
void OnBroadcastCustomMessage(QString realm, obs_data_t* data);
|
||||
|
||||
bool HeartbeatIsActive;
|
||||
|
||||
private slots:
|
||||
@ -65,7 +72,6 @@ private:
|
||||
bool pulse;
|
||||
|
||||
uint64_t _streamStarttime;
|
||||
uint64_t _recStarttime;
|
||||
|
||||
uint64_t _lastBytesSent;
|
||||
uint64_t _lastBytesSentTime;
|
||||
@ -93,6 +99,8 @@ private:
|
||||
void OnRecordingStarted();
|
||||
void OnRecordingStopping();
|
||||
void OnRecordingStopped();
|
||||
void OnRecordingPaused();
|
||||
void OnRecordingResumed();
|
||||
|
||||
void OnReplayStarting();
|
||||
void OnReplayStarted();
|
||||
@ -121,6 +129,7 @@ private:
|
||||
|
||||
static void OnSourceFilterAdded(void* param, calldata_t* data);
|
||||
static void OnSourceFilterRemoved(void* param, calldata_t* data);
|
||||
static void OnSourceFilterVisibilityChanged(void* param, calldata_t* data);
|
||||
static void OnSourceFilterOrderChanged(void* param, calldata_t* data);
|
||||
|
||||
static void OnSceneReordered(void* param, calldata_t* data);
|
||||
|
@ -36,6 +36,8 @@ QHash<QString, HandlerResponse(*)(WSRequestHandler*)> WSRequestHandler::messageM
|
||||
{ "SetFilenameFormatting", WSRequestHandler::HandleSetFilenameFormatting },
|
||||
{ "GetFilenameFormatting", WSRequestHandler::HandleGetFilenameFormatting },
|
||||
|
||||
{ "BroadcastCustomMessage", WSRequestHandler::HandleBroadcastCustomMessage },
|
||||
|
||||
{ "SetCurrentScene", WSRequestHandler::HandleSetCurrentScene },
|
||||
{ "GetCurrentScene", WSRequestHandler::HandleGetCurrentScene },
|
||||
{ "GetSceneList", WSRequestHandler::HandleGetSceneList },
|
||||
@ -55,10 +57,14 @@ QHash<QString, HandlerResponse(*)(WSRequestHandler*)> WSRequestHandler::messageM
|
||||
{ "GetStreamingStatus", WSRequestHandler::HandleGetStreamingStatus },
|
||||
{ "StartStopStreaming", WSRequestHandler::HandleStartStopStreaming },
|
||||
{ "StartStopRecording", WSRequestHandler::HandleStartStopRecording },
|
||||
|
||||
{ "StartStreaming", WSRequestHandler::HandleStartStreaming },
|
||||
{ "StopStreaming", WSRequestHandler::HandleStopStreaming },
|
||||
|
||||
{ "StartRecording", WSRequestHandler::HandleStartRecording },
|
||||
{ "StopRecording", WSRequestHandler::HandleStopRecording },
|
||||
{ "PauseRecording", WSRequestHandler::HandlePauseRecording },
|
||||
{ "ResumeRecording", WSRequestHandler::HandleResumeRecording },
|
||||
|
||||
{ "StartStopReplayBuffer", WSRequestHandler::HandleStartStopReplayBuffer },
|
||||
{ "StartReplayBuffer", WSRequestHandler::HandleStartReplayBuffer },
|
||||
@ -89,11 +95,13 @@ QHash<QString, HandlerResponse(*)(WSRequestHandler*)> WSRequestHandler::messageM
|
||||
{ "TakeSourceScreenshot", WSRequestHandler::HandleTakeSourceScreenshot },
|
||||
|
||||
{ "GetSourceFilters", WSRequestHandler::HandleGetSourceFilters },
|
||||
{ "GetSourceFilterInfo", WSRequestHandler::HandleGetSourceFilterInfo },
|
||||
{ "AddFilterToSource", WSRequestHandler::HandleAddFilterToSource },
|
||||
{ "RemoveFilterFromSource", WSRequestHandler::HandleRemoveFilterFromSource },
|
||||
{ "ReorderSourceFilter", WSRequestHandler::HandleReorderSourceFilter },
|
||||
{ "MoveSourceFilter", WSRequestHandler::HandleMoveSourceFilter },
|
||||
{ "SetSourceFilterSettings", WSRequestHandler::HandleSetSourceFilterSettings },
|
||||
{ "SetSourceFilterVisibility", WSRequestHandler::HandleSetSourceFilterVisibility },
|
||||
|
||||
{ "SetCurrentSceneCollection", WSRequestHandler::HandleSetCurrentSceneCollection },
|
||||
{ "GetCurrentSceneCollection", WSRequestHandler::HandleGetCurrentSceneCollection },
|
||||
@ -125,7 +133,12 @@ QHash<QString, HandlerResponse(*)(WSRequestHandler*)> WSRequestHandler::messageM
|
||||
{ "GetTextFreetype2Properties", WSRequestHandler::HandleGetTextFreetype2Properties },
|
||||
|
||||
{ "GetBrowserSourceProperties", WSRequestHandler::HandleGetBrowserSourceProperties },
|
||||
{ "SetBrowserSourceProperties", WSRequestHandler::HandleSetBrowserSourceProperties }
|
||||
{ "SetBrowserSourceProperties", WSRequestHandler::HandleSetBrowserSourceProperties },
|
||||
|
||||
{ "ListOutputs", WSRequestHandler::HandleListOutputs },
|
||||
{ "GetOutputInfo", WSRequestHandler::HandleGetOutputInfo },
|
||||
{ "StartOutput", WSRequestHandler::HandleStartOutput },
|
||||
{ "StopOutput", WSRequestHandler::HandleStopOutput }
|
||||
};
|
||||
|
||||
QSet<QString> WSRequestHandler::authNotRequired {
|
||||
@ -196,9 +209,9 @@ HandlerResponse WSRequestHandler::SendOKResponse(obs_data_t* additionalFields) {
|
||||
return SendResponse("ok", additionalFields);
|
||||
}
|
||||
|
||||
HandlerResponse WSRequestHandler::SendErrorResponse(const char* errorMessage) {
|
||||
HandlerResponse WSRequestHandler::SendErrorResponse(QString errorMessage) {
|
||||
OBSDataAutoRelease fields = obs_data_create();
|
||||
obs_data_set_string(fields, "error", errorMessage);
|
||||
obs_data_set_string(fields, "error", errorMessage.toUtf8().constData());
|
||||
|
||||
return SendResponse("error", fields);
|
||||
}
|
||||
@ -219,9 +232,57 @@ HandlerResponse WSRequestHandler::SendResponse(const char* status, obs_data_t* f
|
||||
return response;
|
||||
}
|
||||
|
||||
bool WSRequestHandler::hasField(QString name) {
|
||||
if (!data || name.isEmpty() || name.isNull())
|
||||
bool WSRequestHandler::hasField(QString name, obs_data_type expectedFieldType, obs_data_number_type expectedNumberType) {
|
||||
if (!data || name.isEmpty() || name.isNull()) {
|
||||
return false;
|
||||
|
||||
return obs_data_has_user_value(data, name.toUtf8());
|
||||
}
|
||||
|
||||
OBSDataItemAutoRelease dataItem = obs_data_item_byname(data, name.toUtf8());
|
||||
if (!dataItem) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (expectedFieldType != OBS_DATA_NULL) {
|
||||
obs_data_type fieldType = obs_data_item_gettype(dataItem);
|
||||
if (fieldType != expectedFieldType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fieldType == OBS_DATA_NUMBER && expectedNumberType != OBS_DATA_NUM_INVALID) {
|
||||
obs_data_number_type numberType = obs_data_item_numtype(dataItem);
|
||||
if (numberType != expectedNumberType) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WSRequestHandler::hasBool(QString fieldName) {
|
||||
return this->hasField(fieldName, OBS_DATA_BOOLEAN);
|
||||
}
|
||||
|
||||
bool WSRequestHandler::hasString(QString fieldName) {
|
||||
return this->hasField(fieldName, OBS_DATA_STRING);
|
||||
}
|
||||
|
||||
bool WSRequestHandler::hasNumber(QString fieldName, obs_data_number_type expectedNumberType) {
|
||||
return this->hasField(fieldName, OBS_DATA_NUMBER, expectedNumberType);
|
||||
}
|
||||
|
||||
bool WSRequestHandler::hasInteger(QString fieldName) {
|
||||
return this->hasNumber(fieldName, OBS_DATA_NUM_INT);
|
||||
}
|
||||
|
||||
bool WSRequestHandler::hasDouble(QString fieldName) {
|
||||
return this->hasNumber(fieldName, OBS_DATA_NUM_DOUBLE);
|
||||
}
|
||||
|
||||
bool WSRequestHandler::hasArray(QString fieldName) {
|
||||
return this->hasField(fieldName, OBS_DATA_ARRAY);
|
||||
}
|
||||
|
||||
bool WSRequestHandler::hasObject(QString fieldName) {
|
||||
return this->hasField(fieldName, OBS_DATA_OBJECT);
|
||||
}
|
||||
|
@ -41,7 +41,25 @@ class WSRequestHandler : public QObject {
|
||||
explicit WSRequestHandler(ConnectionProperties& connProperties);
|
||||
~WSRequestHandler();
|
||||
std::string processIncomingMessage(std::string& textMessage);
|
||||
bool hasField(QString name);
|
||||
|
||||
bool hasField(QString fieldName, obs_data_type expectedFieldType = OBS_DATA_NULL,
|
||||
obs_data_number_type expectedNumberType = OBS_DATA_NUM_INVALID);
|
||||
bool hasBool(QString fieldName);
|
||||
bool hasString(QString fieldName);
|
||||
bool hasNumber(QString fieldName, obs_data_number_type expectedNumberType = OBS_DATA_NUM_INVALID);
|
||||
bool hasInteger(QString fieldName);
|
||||
bool hasDouble(QString fieldName);
|
||||
bool hasArray(QString fieldName);
|
||||
bool hasObject(QString fieldName);
|
||||
|
||||
HandlerResponse SendOKResponse(obs_data_t* additionalFields = nullptr);
|
||||
HandlerResponse SendErrorResponse(QString errorMessage);
|
||||
HandlerResponse SendErrorResponse(obs_data_t* additionalFields = nullptr);
|
||||
HandlerResponse SendResponse(const char* status, obs_data_t* additionalFields = nullptr);
|
||||
|
||||
obs_data_t* parameters() {
|
||||
return this->data;
|
||||
}
|
||||
|
||||
private:
|
||||
const char* _messageId;
|
||||
@ -51,11 +69,6 @@ class WSRequestHandler : public QObject {
|
||||
|
||||
HandlerResponse processRequest(std::string& textMessage);
|
||||
|
||||
HandlerResponse SendOKResponse(obs_data_t* additionalFields = nullptr);
|
||||
HandlerResponse SendErrorResponse(const char* errorMessage);
|
||||
HandlerResponse SendErrorResponse(obs_data_t* additionalFields = nullptr);
|
||||
HandlerResponse SendResponse(const char* status, obs_data_t* additionalFields = nullptr);
|
||||
|
||||
static QHash<QString, HandlerResponse(*)(WSRequestHandler*)> messageMap;
|
||||
static QSet<QString> authNotRequired;
|
||||
|
||||
@ -70,6 +83,8 @@ class WSRequestHandler : public QObject {
|
||||
static HandlerResponse HandleSetFilenameFormatting(WSRequestHandler* req);
|
||||
static HandlerResponse HandleGetFilenameFormatting(WSRequestHandler* req);
|
||||
|
||||
static HandlerResponse HandleBroadcastCustomMessage(WSRequestHandler* req);
|
||||
|
||||
static HandlerResponse HandleSetCurrentScene(WSRequestHandler* req);
|
||||
static HandlerResponse HandleGetCurrentScene(WSRequestHandler* req);
|
||||
static HandlerResponse HandleGetSceneList(WSRequestHandler* req);
|
||||
@ -88,10 +103,14 @@ class WSRequestHandler : public QObject {
|
||||
static HandlerResponse HandleGetStreamingStatus(WSRequestHandler* req);
|
||||
static HandlerResponse HandleStartStopStreaming(WSRequestHandler* req);
|
||||
static HandlerResponse HandleStartStopRecording(WSRequestHandler* req);
|
||||
|
||||
static HandlerResponse HandleStartStreaming(WSRequestHandler* req);
|
||||
static HandlerResponse HandleStopStreaming(WSRequestHandler* req);
|
||||
|
||||
static HandlerResponse HandleStartRecording(WSRequestHandler* req);
|
||||
static HandlerResponse HandleStopRecording(WSRequestHandler* req);
|
||||
static HandlerResponse HandlePauseRecording(WSRequestHandler* req);
|
||||
static HandlerResponse HandleResumeRecording(WSRequestHandler* req);
|
||||
|
||||
static HandlerResponse HandleStartStopReplayBuffer(WSRequestHandler* req);
|
||||
static HandlerResponse HandleStartReplayBuffer(WSRequestHandler* req);
|
||||
@ -120,11 +139,13 @@ class WSRequestHandler : public QObject {
|
||||
static HandlerResponse HandleTakeSourceScreenshot(WSRequestHandler* req);
|
||||
|
||||
static HandlerResponse HandleGetSourceFilters(WSRequestHandler* req);
|
||||
static HandlerResponse HandleGetSourceFilterInfo(WSRequestHandler* req);
|
||||
static HandlerResponse HandleAddFilterToSource(WSRequestHandler* req);
|
||||
static HandlerResponse HandleRemoveFilterFromSource(WSRequestHandler* req);
|
||||
static HandlerResponse HandleReorderSourceFilter(WSRequestHandler* req);
|
||||
static HandlerResponse HandleMoveSourceFilter(WSRequestHandler* req);
|
||||
static HandlerResponse HandleSetSourceFilterSettings(WSRequestHandler* req);
|
||||
static HandlerResponse HandleSetSourceFilterVisibility(WSRequestHandler* req);
|
||||
|
||||
static HandlerResponse HandleSetCurrentSceneCollection(WSRequestHandler* req);
|
||||
static HandlerResponse HandleGetCurrentSceneCollection(WSRequestHandler* req);
|
||||
@ -160,4 +181,9 @@ class WSRequestHandler : public QObject {
|
||||
|
||||
static HandlerResponse HandleSetBrowserSourceProperties(WSRequestHandler* req);
|
||||
static HandlerResponse HandleGetBrowserSourceProperties(WSRequestHandler* req);
|
||||
|
||||
static HandlerResponse HandleListOutputs(WSRequestHandler* req);
|
||||
static HandlerResponse HandleGetOutputInfo(WSRequestHandler* req);
|
||||
static HandlerResponse HandleStartOutput(WSRequestHandler* req);
|
||||
static HandlerResponse HandleStopOutput(WSRequestHandler* req);
|
||||
};
|
||||
|
@ -231,6 +231,40 @@ HandlerResponse WSRequestHandler::HandleGetStats(WSRequestHandler* req) {
|
||||
return req->SendOKResponse(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast custom message to all connected WebSocket clients
|
||||
*
|
||||
* @param {String} `realm` Identifier to be choosen by the client
|
||||
* @param {Object} `data` User-defined data
|
||||
*
|
||||
* @api requests
|
||||
* @name BroadcastCustomMessage
|
||||
* @category general
|
||||
* @since 4.7.0
|
||||
*/
|
||||
HandlerResponse WSRequestHandler::HandleBroadcastCustomMessage(WSRequestHandler* req) {
|
||||
if (!req->hasField("realm") || !req->hasField("data")) {
|
||||
return req->SendErrorResponse("missing request parameters");
|
||||
}
|
||||
|
||||
QString realm = obs_data_get_string(req->data, "realm");
|
||||
OBSDataAutoRelease data = obs_data_get_obj(req->data, "data");
|
||||
|
||||
if (realm.isEmpty()) {
|
||||
return req->SendErrorResponse("realm not specified!");
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return req->SendErrorResponse("data not specified!");
|
||||
}
|
||||
|
||||
auto events = GetEventsSystem();
|
||||
events->OnBroadcastCustomMessage(realm, data);
|
||||
|
||||
return req->SendOKResponse();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get basic OBS video information
|
||||
*
|
||||
|
181
src/WSRequestHandler_Outputs.cpp
Normal file
181
src/WSRequestHandler_Outputs.cpp
Normal file
@ -0,0 +1,181 @@
|
||||
#include "WSRequestHandler.h"
|
||||
|
||||
/**
|
||||
* @typedef {Object} `Output`
|
||||
* @property {String} `name` Output name
|
||||
* @property {String} `type` Output type/kind
|
||||
* @property {int} `width` Video output width
|
||||
* @property {int} `height` Video output height
|
||||
* @property {Object} `flags` Output flags
|
||||
* @property {int} `flags.rawValue` Raw flags value
|
||||
* @property {boolean} `flags.audio` Output uses audio
|
||||
* @property {boolean} `flags.video` Output uses video
|
||||
* @property {boolean} `flags.encoded` Output is encoded
|
||||
* @property {boolean} `flags.multiTrack` Output uses several audio tracks
|
||||
* @property {boolean} `flags.service` Output uses a service
|
||||
* @property {Object} `settings` Output name
|
||||
* @property {boolean} `active` Output status (active or not)
|
||||
* @property {boolean} `reconnecting` Output reconnection status (reconnecting or not)
|
||||
* @property {double} `congestion` Output congestion
|
||||
* @property {int} `totalFrames` Number of frames sent
|
||||
* @property {int} `droppedFrames` Number of frames dropped
|
||||
* @property {int} `totalBytes` Total bytes sent
|
||||
*/
|
||||
obs_data_t* getOutputInfo(obs_output_t* output)
|
||||
{
|
||||
if (!output) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
OBSDataAutoRelease settings = obs_output_get_settings(output);
|
||||
|
||||
uint32_t rawFlags = obs_output_get_flags(output);
|
||||
OBSDataAutoRelease flags = obs_data_create();
|
||||
obs_data_set_int(flags, "rawValue", rawFlags);
|
||||
obs_data_set_bool(flags, "audio", rawFlags & OBS_OUTPUT_AUDIO);
|
||||
obs_data_set_bool(flags, "video", rawFlags & OBS_OUTPUT_VIDEO);
|
||||
obs_data_set_bool(flags, "encoded", rawFlags & OBS_OUTPUT_ENCODED);
|
||||
obs_data_set_bool(flags, "multiTrack", rawFlags & OBS_OUTPUT_MULTI_TRACK);
|
||||
obs_data_set_bool(flags, "service", rawFlags & OBS_OUTPUT_SERVICE);
|
||||
|
||||
obs_data_t* data = obs_data_create();
|
||||
|
||||
obs_data_set_string(data, "name", obs_output_get_name(output));
|
||||
obs_data_set_string(data, "type", obs_output_get_id(output));
|
||||
obs_data_set_int(data, "width", obs_output_get_width(output));
|
||||
obs_data_set_int(data, "height", obs_output_get_height(output));
|
||||
obs_data_set_obj(data, "flags", flags);
|
||||
obs_data_set_obj(data, "settings", settings);
|
||||
|
||||
obs_data_set_bool(data, "active", obs_output_active(output));
|
||||
obs_data_set_bool(data, "reconnecting", obs_output_reconnecting(output));
|
||||
obs_data_set_double(data, "congestion", obs_output_get_congestion(output));
|
||||
obs_data_set_int(data, "totalFrames", obs_output_get_total_frames(output));
|
||||
obs_data_set_int(data, "droppedFrames", obs_output_get_frames_dropped(output));
|
||||
obs_data_set_int(data, "totalBytes", obs_output_get_total_bytes(output));
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
HandlerResponse findOutputOrFail(WSRequestHandler* req, std::function<HandlerResponse (obs_output_t*)> callback)
|
||||
{
|
||||
if (!req->hasField("outputName")) {
|
||||
return req->SendErrorResponse("missing request parameters");
|
||||
}
|
||||
|
||||
const char* outputName = obs_data_get_string(req->parameters(), "outputName");
|
||||
OBSOutputAutoRelease output = obs_get_output_by_name(outputName);
|
||||
if (!output) {
|
||||
return req->SendErrorResponse("specified output doesn't exist");
|
||||
}
|
||||
|
||||
return callback(output);
|
||||
}
|
||||
|
||||
/**
|
||||
* List existing outputs
|
||||
*
|
||||
* @return {Array<Output>} `outputs` Outputs list
|
||||
*
|
||||
* @api requests
|
||||
* @name ListOutputs
|
||||
* @category outputs
|
||||
* @since 4.7.0
|
||||
*/
|
||||
HandlerResponse WSRequestHandler::HandleListOutputs(WSRequestHandler* req)
|
||||
{
|
||||
OBSDataArrayAutoRelease outputs = obs_data_array_create();
|
||||
|
||||
obs_enum_outputs([](void* param, obs_output_t* output) {
|
||||
obs_data_array_t* outputs = reinterpret_cast<obs_data_array_t*>(param);
|
||||
|
||||
OBSDataAutoRelease outputInfo = getOutputInfo(output);
|
||||
obs_data_array_push_back(outputs, outputInfo);
|
||||
|
||||
return true;
|
||||
}, outputs);
|
||||
|
||||
OBSDataAutoRelease fields = obs_data_create();
|
||||
obs_data_set_array(fields, "outputs", outputs);
|
||||
return req->SendOKResponse(fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information about a single output
|
||||
*
|
||||
* @param {String} `outputName` Output name
|
||||
*
|
||||
* @return {Output} `outputInfo` Output info
|
||||
*
|
||||
* @api requests
|
||||
* @name GetOutputInfo
|
||||
* @category outputs
|
||||
* @since 4.7.0
|
||||
*/
|
||||
HandlerResponse WSRequestHandler::HandleGetOutputInfo(WSRequestHandler* req)
|
||||
{
|
||||
return findOutputOrFail(req, [req](obs_output_t* output) {
|
||||
OBSDataAutoRelease outputInfo = getOutputInfo(output);
|
||||
|
||||
OBSDataAutoRelease fields = obs_data_create();
|
||||
obs_data_set_obj(fields, "outputInfo", outputInfo);
|
||||
return req->SendOKResponse(fields);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start an output
|
||||
*
|
||||
* @param {String} `outputName` Output name
|
||||
*
|
||||
* @api requests
|
||||
* @name StartOutput
|
||||
* @category outputs
|
||||
* @since 4.7.0
|
||||
*/
|
||||
HandlerResponse WSRequestHandler::HandleStartOutput(WSRequestHandler* req)
|
||||
{
|
||||
return findOutputOrFail(req, [req](obs_output_t* output) {
|
||||
if (obs_output_active(output)) {
|
||||
return req->SendErrorResponse("output already active");
|
||||
}
|
||||
|
||||
bool success = obs_output_start(output);
|
||||
if (!success) {
|
||||
QString lastError = obs_output_get_last_error(output);
|
||||
QString errorMessage = QString("output start failed: %1").arg(lastError);
|
||||
return req->SendErrorResponse(errorMessage);
|
||||
}
|
||||
|
||||
return req->SendOKResponse();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop an output
|
||||
*
|
||||
* @param {String} `outputName` Output name
|
||||
* @param {boolean (optional)} `force` Force stop (default: false)
|
||||
*
|
||||
* @api requests
|
||||
* @name StopOutput
|
||||
* @category outputs
|
||||
* @since 4.7.0
|
||||
*/
|
||||
HandlerResponse WSRequestHandler::HandleStopOutput(WSRequestHandler* req)
|
||||
{
|
||||
return findOutputOrFail(req, [req](obs_output_t* output) {
|
||||
if (!obs_output_active(output)) {
|
||||
return req->SendErrorResponse("output not active");
|
||||
}
|
||||
|
||||
bool forceStop = obs_data_get_bool(req->data, "force");
|
||||
if (forceStop) {
|
||||
obs_output_force_stop(output);
|
||||
} else {
|
||||
obs_output_stop(output);
|
||||
}
|
||||
|
||||
return req->SendOKResponse();
|
||||
});
|
||||
}
|
@ -1,6 +1,20 @@
|
||||
#include "WSRequestHandler.h"
|
||||
|
||||
#include <util/platform.h>
|
||||
#include "Utils.h"
|
||||
|
||||
#include "WSRequestHandler.h"
|
||||
HandlerResponse ifCanPause(WSRequestHandler* req, std::function<HandlerResponse()> callback)
|
||||
{
|
||||
if (!obs_frontend_recording_active()) {
|
||||
return req->SendErrorResponse("recording is not active");
|
||||
}
|
||||
|
||||
if (!Utils::RecordingPauseSupported()) {
|
||||
return req->SendErrorResponse("recording pauses are not available in this version of OBS Studio");
|
||||
}
|
||||
|
||||
return callback();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle recording on or off.
|
||||
@ -11,11 +25,7 @@
|
||||
* @since 0.3
|
||||
*/
|
||||
HandlerResponse WSRequestHandler::HandleStartStopRecording(WSRequestHandler* req) {
|
||||
if (obs_frontend_recording_active())
|
||||
obs_frontend_recording_stop();
|
||||
else
|
||||
obs_frontend_recording_start();
|
||||
|
||||
(obs_frontend_recording_active() ? obs_frontend_recording_stop() : obs_frontend_recording_start());
|
||||
return req->SendOKResponse();
|
||||
}
|
||||
|
||||
@ -29,12 +39,12 @@ HandlerResponse WSRequestHandler::HandleStartStopRecording(WSRequestHandler* req
|
||||
* @since 4.1.0
|
||||
*/
|
||||
HandlerResponse WSRequestHandler::HandleStartRecording(WSRequestHandler* req) {
|
||||
if (obs_frontend_recording_active() == false) {
|
||||
obs_frontend_recording_start();
|
||||
return req->SendOKResponse();
|
||||
} else {
|
||||
if (obs_frontend_recording_active()) {
|
||||
return req->SendErrorResponse("recording already active");
|
||||
}
|
||||
|
||||
obs_frontend_recording_start();
|
||||
return req->SendOKResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -47,12 +57,52 @@ HandlerResponse WSRequestHandler::HandleStartRecording(WSRequestHandler* req) {
|
||||
* @since 4.1.0
|
||||
*/
|
||||
HandlerResponse WSRequestHandler::HandleStopRecording(WSRequestHandler* req) {
|
||||
if (obs_frontend_recording_active() == true) {
|
||||
obs_frontend_recording_stop();
|
||||
return req->SendOKResponse();
|
||||
} else {
|
||||
if (!obs_frontend_recording_active()) {
|
||||
return req->SendErrorResponse("recording not active");
|
||||
}
|
||||
|
||||
obs_frontend_recording_stop();
|
||||
return req->SendOKResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause the current recording.
|
||||
* Returns an error if recording is not active or already paused.
|
||||
*
|
||||
* @api requests
|
||||
* @name PauseRecording
|
||||
* @category recording
|
||||
* @since 4.7.0
|
||||
*/
|
||||
HandlerResponse WSRequestHandler::HandlePauseRecording(WSRequestHandler* req) {
|
||||
return ifCanPause(req, [req]() {
|
||||
if (Utils::RecordingPaused()) {
|
||||
return req->SendErrorResponse("recording already paused");
|
||||
}
|
||||
|
||||
Utils::PauseRecording(true);
|
||||
return req->SendOKResponse();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume/unpause the current recording (if paused).
|
||||
* Returns an error if recording is not active or not paused.
|
||||
*
|
||||
* @api requests
|
||||
* @name ResumeRecording
|
||||
* @category recording
|
||||
* @since 4.7.0
|
||||
*/
|
||||
HandlerResponse WSRequestHandler::HandleResumeRecording(WSRequestHandler* req) {
|
||||
return ifCanPause(req, [req]() {
|
||||
if (!Utils::RecordingPaused()) {
|
||||
return req->SendErrorResponse("recording is not paused");
|
||||
}
|
||||
|
||||
Utils::PauseRecording(false);
|
||||
return req->SendOKResponse();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -63,7 +113,6 @@ HandlerResponse WSRequestHandler::HandleStartRecording(WSRequestHandler* req) {
|
||||
* in progress, the change won't be applied immediately and will be
|
||||
* effective on the next recording.
|
||||
*
|
||||
*
|
||||
* @param {String} `rec-folder` Path of the recording folder.
|
||||
*
|
||||
* @api requests
|
||||
|
@ -49,13 +49,12 @@ HandlerResponse WSRequestHandler::HandleGetSceneItemProperties(WSRequestHandler*
|
||||
}
|
||||
|
||||
QString sceneName = obs_data_get_string(req->data, "scene-name");
|
||||
OBSSourceAutoRelease scene = Utils::GetSceneFromNameOrCurrent(sceneName);
|
||||
OBSScene scene = Utils::GetSceneFromNameOrCurrent(sceneName);
|
||||
if (!scene) {
|
||||
return req->SendErrorResponse("requested scene doesn't exist");
|
||||
}
|
||||
|
||||
OBSSceneItemAutoRelease sceneItem =
|
||||
Utils::GetSceneItemFromName(scene, itemName);
|
||||
OBSSceneItemAutoRelease sceneItem = Utils::GetSceneItemFromName(scene, itemName);
|
||||
if (!sceneItem) {
|
||||
return req->SendErrorResponse("specified scene item doesn't exist");
|
||||
}
|
||||
@ -105,7 +104,7 @@ HandlerResponse WSRequestHandler::HandleSetSceneItemProperties(WSRequestHandler*
|
||||
}
|
||||
|
||||
QString sceneName = obs_data_get_string(req->data, "scene-name");
|
||||
OBSSourceAutoRelease scene = Utils::GetSceneFromNameOrCurrent(sceneName);
|
||||
OBSScene scene = Utils::GetSceneFromNameOrCurrent(sceneName);
|
||||
if (!scene) {
|
||||
return req->SendErrorResponse("requested scene doesn't exist");
|
||||
}
|
||||
@ -283,7 +282,7 @@ HandlerResponse WSRequestHandler::HandleResetSceneItem(WSRequestHandler* req) {
|
||||
}
|
||||
|
||||
const char* sceneName = obs_data_get_string(req->data, "scene-name");
|
||||
OBSSourceAutoRelease scene = Utils::GetSceneFromNameOrCurrent(sceneName);
|
||||
OBSScene scene = Utils::GetSceneFromNameOrCurrent(sceneName);
|
||||
if (!scene) {
|
||||
return req->SendErrorResponse("requested scene doesn't exist");
|
||||
}
|
||||
@ -329,7 +328,7 @@ HandlerResponse WSRequestHandler::HandleSetSceneItemRender(WSRequestHandler* req
|
||||
}
|
||||
|
||||
const char* sceneName = obs_data_get_string(req->data, "scene-name");
|
||||
OBSSourceAutoRelease scene = Utils::GetSceneFromNameOrCurrent(sceneName);
|
||||
OBSScene scene = Utils::GetSceneFromNameOrCurrent(sceneName);
|
||||
if (!scene) {
|
||||
return req->SendErrorResponse("requested scene doesn't exist");
|
||||
}
|
||||
@ -371,7 +370,7 @@ HandlerResponse WSRequestHandler::HandleSetSceneItemPosition(WSRequestHandler* r
|
||||
}
|
||||
|
||||
QString sceneName = obs_data_get_string(req->data, "scene-name");
|
||||
OBSSourceAutoRelease scene = Utils::GetSceneFromNameOrCurrent(sceneName);
|
||||
OBSScene scene = Utils::GetSceneFromNameOrCurrent(sceneName);
|
||||
if (!scene) {
|
||||
return req->SendErrorResponse("requested scene could not be found");
|
||||
}
|
||||
@ -419,7 +418,7 @@ HandlerResponse WSRequestHandler::HandleSetSceneItemTransform(WSRequestHandler*
|
||||
}
|
||||
|
||||
QString sceneName = obs_data_get_string(req->data, "scene-name");
|
||||
OBSSourceAutoRelease scene = Utils::GetSceneFromNameOrCurrent(sceneName);
|
||||
OBSScene scene = Utils::GetSceneFromNameOrCurrent(sceneName);
|
||||
if (!scene) {
|
||||
return req->SendErrorResponse("requested scene doesn't exist");
|
||||
}
|
||||
@ -471,7 +470,7 @@ HandlerResponse WSRequestHandler::HandleSetSceneItemCrop(WSRequestHandler* req)
|
||||
}
|
||||
|
||||
QString sceneName = obs_data_get_string(req->data, "scene-name");
|
||||
OBSSourceAutoRelease scene = Utils::GetSceneFromNameOrCurrent(sceneName);
|
||||
OBSScene scene = Utils::GetSceneFromNameOrCurrent(sceneName);
|
||||
if (!scene) {
|
||||
return req->SendErrorResponse("requested scene doesn't exist");
|
||||
}
|
||||
@ -511,7 +510,7 @@ HandlerResponse WSRequestHandler::HandleDeleteSceneItem(WSRequestHandler* req) {
|
||||
}
|
||||
|
||||
const char* sceneName = obs_data_get_string(req->data, "scene");
|
||||
OBSSourceAutoRelease scene = Utils::GetSceneFromNameOrCurrent(sceneName);
|
||||
OBSScene scene = Utils::GetSceneFromNameOrCurrent(sceneName);
|
||||
if (!scene) {
|
||||
return req->SendErrorResponse("requested scene doesn't exist");
|
||||
}
|
||||
@ -564,13 +563,13 @@ HandlerResponse WSRequestHandler::HandleDuplicateSceneItem(WSRequestHandler* req
|
||||
}
|
||||
|
||||
const char* fromSceneName = obs_data_get_string(req->data, "fromScene");
|
||||
OBSSourceAutoRelease fromScene = Utils::GetSceneFromNameOrCurrent(fromSceneName);
|
||||
OBSScene fromScene = Utils::GetSceneFromNameOrCurrent(fromSceneName);
|
||||
if (!fromScene) {
|
||||
return req->SendErrorResponse("requested fromScene doesn't exist");
|
||||
}
|
||||
|
||||
const char* toSceneName = obs_data_get_string(req->data, "toScene");
|
||||
OBSSourceAutoRelease toScene = Utils::GetSceneFromNameOrCurrent(toSceneName);
|
||||
OBSScene toScene = Utils::GetSceneFromNameOrCurrent(toSceneName);
|
||||
if (!toScene) {
|
||||
return req->SendErrorResponse("requested toScene doesn't exist");
|
||||
}
|
||||
@ -586,7 +585,7 @@ HandlerResponse WSRequestHandler::HandleDuplicateSceneItem(WSRequestHandler* req
|
||||
data.referenceItem = referenceItem;
|
||||
|
||||
obs_enter_graphics();
|
||||
obs_scene_atomic_update(obs_scene_from_source(toScene), DuplicateSceneItem, &data);
|
||||
obs_scene_atomic_update(toScene, DuplicateSceneItem, &data);
|
||||
obs_leave_graphics();
|
||||
|
||||
obs_sceneitem_t *newItem = data.newItem;
|
||||
@ -600,7 +599,7 @@ HandlerResponse WSRequestHandler::HandleDuplicateSceneItem(WSRequestHandler* req
|
||||
|
||||
OBSDataAutoRelease responseData = obs_data_create();
|
||||
obs_data_set_obj(responseData, "item", itemData);
|
||||
obs_data_set_string(responseData, "scene", obs_source_get_name(toScene));
|
||||
obs_data_set_string(responseData, "scene", obs_source_get_name(obs_scene_get_source(toScene)));
|
||||
|
||||
return req->SendOKResponse(responseData);
|
||||
}
|
||||
|
@ -94,7 +94,7 @@ HandlerResponse WSRequestHandler::HandleGetSceneList(WSRequestHandler* req) {
|
||||
*/
|
||||
HandlerResponse WSRequestHandler::HandleReorderSceneItems(WSRequestHandler* req) {
|
||||
QString sceneName = obs_data_get_string(req->data, "scene");
|
||||
OBSSourceAutoRelease scene = Utils::GetSceneFromNameOrCurrent(sceneName);
|
||||
OBSScene scene = Utils::GetSceneFromNameOrCurrent(sceneName);
|
||||
if (!scene) {
|
||||
return req->SendErrorResponse("requested scene doesn't exist");
|
||||
}
|
||||
@ -104,37 +104,46 @@ HandlerResponse WSRequestHandler::HandleReorderSceneItems(WSRequestHandler* req)
|
||||
return req->SendErrorResponse("sceneItem order not specified");
|
||||
}
|
||||
|
||||
size_t count = obs_data_array_count(items);
|
||||
struct reorder_context {
|
||||
obs_data_array_t* items;
|
||||
bool success;
|
||||
QString errorMessage;
|
||||
};
|
||||
|
||||
std::vector<obs_sceneitem_t*> newOrder;
|
||||
newOrder.reserve(count);
|
||||
struct reorder_context ctx;
|
||||
ctx.success = false;
|
||||
ctx.items = items;
|
||||
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
OBSDataAutoRelease item = obs_data_array_item(items, i);
|
||||
obs_scene_atomic_update(scene, [](void* param, obs_scene_t* scene) {
|
||||
auto ctx = reinterpret_cast<struct reorder_context*>(param);
|
||||
|
||||
QVector<struct obs_sceneitem_order_info> orderList;
|
||||
struct obs_sceneitem_order_info info;
|
||||
|
||||
size_t itemCount = obs_data_array_count(ctx->items);
|
||||
for (int i = 0; i < itemCount; i++) {
|
||||
OBSDataAutoRelease item = obs_data_array_item(ctx->items, i);
|
||||
|
||||
OBSSceneItemAutoRelease sceneItem = Utils::GetSceneItemFromItem(scene, item);
|
||||
obs_sceneitem_release(sceneItem); // ref dec
|
||||
|
||||
if (!sceneItem) {
|
||||
return req->SendErrorResponse("Invalid sceneItem id or name specified");
|
||||
ctx->success = false;
|
||||
ctx->errorMessage = "Invalid sceneItem id or name specified";
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t j = 0; j <= i; ++j) {
|
||||
if (sceneItem == newOrder[j]) {
|
||||
return req->SendErrorResponse("Duplicate sceneItem in specified order");
|
||||
}
|
||||
info.group = nullptr;
|
||||
info.item = sceneItem;
|
||||
orderList.insert(0, info);
|
||||
}
|
||||
|
||||
newOrder.push_back(sceneItem);
|
||||
ctx->success = obs_scene_reorder_items2(scene, orderList.data(), orderList.size());
|
||||
if (!ctx->success) {
|
||||
ctx->errorMessage = "Invalid sceneItem order";
|
||||
}
|
||||
}, &ctx);
|
||||
|
||||
bool success = obs_scene_reorder_items(obs_scene_from_source(scene), newOrder.data(), count);
|
||||
if (!success) {
|
||||
return req->SendErrorResponse("Invalid sceneItem order");
|
||||
}
|
||||
|
||||
for (auto const& item: newOrder) {
|
||||
obs_sceneitem_release(item);
|
||||
if (!ctx.success) {
|
||||
return req->SendErrorResponse(ctx.errorMessage);
|
||||
}
|
||||
|
||||
return req->SendOKResponse();
|
||||
|
@ -70,19 +70,19 @@ HandlerResponse WSRequestHandler::HandleGetSourcesList(WSRequestHandler* req)
|
||||
/**
|
||||
* Get a list of all available sources types
|
||||
*
|
||||
* @return {Array<Object>} `ids` Array of source types
|
||||
* @return {String} `ids.*.typeId` Non-unique internal source type ID
|
||||
* @return {String} `ids.*.displayName` Display name of the source type
|
||||
* @return {String} `ids.*.type` Type. Value is one of the following: "input", "filter", "transition" or "other"
|
||||
* @return {Object} `ids.*.defaultSettings` Default settings of this source type
|
||||
* @return {Object} `ids.*.caps` Source type capabilities
|
||||
* @return {Boolean} `ids.*.caps.isAsync` True if source of this type provide frames asynchronously
|
||||
* @return {Boolean} `ids.*.caps.hasVideo` True if sources of this type provide video
|
||||
* @return {Boolean} `ids.*.caps.hasAudio` True if sources of this type provide audio
|
||||
* @return {Boolean} `ids.*.caps.canInteract` True if interaction with this sources of this type is possible
|
||||
* @return {Boolean} `ids.*.caps.isComposite` True if sources of this type composite one or more sub-sources
|
||||
* @return {Boolean} `ids.*.caps.doNotDuplicate` True if sources of this type should not be fully duplicated
|
||||
* @return {Boolean} `ids.*.caps.doNotSelfMonitor` True if sources of this type may cause a feedback loop if it's audio is monitored and shouldn't be
|
||||
* @return {Array<Object>} `types` Array of source types
|
||||
* @return {String} `types.*.typeId` Non-unique internal source type ID
|
||||
* @return {String} `types.*.displayName` Display name of the source type
|
||||
* @return {String} `types.*.type` Type. Value is one of the following: "input", "filter", "transition" or "other"
|
||||
* @return {Object} `types.*.defaultSettings` Default settings of this source type
|
||||
* @return {Object} `types.*.caps` Source type capabilities
|
||||
* @return {Boolean} `types.*.caps.isAsync` True if source of this type provide frames asynchronously
|
||||
* @return {Boolean} `types.*.caps.hasVideo` True if sources of this type provide video
|
||||
* @return {Boolean} `types.*.caps.hasAudio` True if sources of this type provide audio
|
||||
* @return {Boolean} `types.*.caps.canInteract` True if interaction with this sources of this type is possible
|
||||
* @return {Boolean} `types.*.caps.isComposite` True if sources of this type composite one or more sub-sources
|
||||
* @return {Boolean} `types.*.caps.doNotDuplicate` True if sources of this type should not be fully duplicated
|
||||
* @return {Boolean} `types.*.caps.doNotSelfMonitor` True if sources of this type may cause a feedback loop if it's audio is monitored and shouldn't be
|
||||
*
|
||||
* @api requests
|
||||
* @name GetSourceTypesList
|
||||
@ -923,7 +923,7 @@ HandlerResponse WSRequestHandler::HandleGetBrowserSourceProperties(WSRequestHand
|
||||
}
|
||||
|
||||
QString sourceId = obs_source_get_id(source);
|
||||
if (sourceId != "browser_source") {
|
||||
if (sourceId != "browser_source" && sourceId != "linuxbrowser-source") {
|
||||
return req->SendErrorResponse("not a browser source");
|
||||
}
|
||||
|
||||
@ -969,7 +969,7 @@ HandlerResponse WSRequestHandler::HandleSetBrowserSourceProperties(WSRequestHand
|
||||
}
|
||||
|
||||
QString sourceId = obs_source_get_id(source);
|
||||
if(sourceId != "browser_source") {
|
||||
if(sourceId != "browser_source" && sourceId != "linuxbrowser-source") {
|
||||
return req->SendErrorResponse("not a browser source");
|
||||
}
|
||||
|
||||
@ -1061,6 +1061,7 @@ HandlerResponse WSRequestHandler::HandleGetSpecialSources(WSRequestHandler* req)
|
||||
* @param {String} `sourceName` Source name
|
||||
*
|
||||
* @return {Array<Object>} `filters` List of filters for the specified source
|
||||
* @return {Boolean} `filters.*.enabled` Filter status (enabled or not)
|
||||
* @return {String} `filters.*.type` Filter type
|
||||
* @return {String} `filters.*.name` Filter name
|
||||
* @return {Object} `filters.*.settings` Filter settings
|
||||
@ -1089,6 +1090,44 @@ HandlerResponse WSRequestHandler::HandleGetSourceFilters(WSRequestHandler* req)
|
||||
return req->SendOKResponse(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* List filters applied to a source
|
||||
*
|
||||
* @param {String} `sourceName` Source name
|
||||
* @param {String} `filterName` Source filter name
|
||||
*
|
||||
* @return {Boolean} `enabled` Filter status (enabled or not)
|
||||
* @return {String} `type` Filter type
|
||||
* @return {String} `name` Filter name
|
||||
* @return {Object} `settings` Filter settings
|
||||
*
|
||||
* @api requests
|
||||
* @name GetSourceFilterInfo
|
||||
* @category sources
|
||||
* @since 4.7.0
|
||||
*/
|
||||
HandlerResponse WSRequestHandler::HandleGetSourceFilterInfo(WSRequestHandler* req)
|
||||
{
|
||||
if (!req->hasField("sourceName") || !req->hasField("filterName")) {
|
||||
return req->SendErrorResponse("missing request parameters");
|
||||
}
|
||||
|
||||
const char* sourceName = obs_data_get_string(req->data, "sourceName");
|
||||
OBSSourceAutoRelease source = obs_get_source_by_name(sourceName);
|
||||
if (!source) {
|
||||
return req->SendErrorResponse("specified source doesn't exist");
|
||||
}
|
||||
|
||||
const char* filterName = obs_data_get_string(req->data, "filterName");
|
||||
OBSSourceAutoRelease filter = obs_source_get_filter_by_name(source, filterName);
|
||||
if (!filter) {
|
||||
return req->SendErrorResponse("specified filter doesn't exist on specified source");
|
||||
}
|
||||
|
||||
OBSDataAutoRelease response = Utils::GetSourceFilterInfo(filter, true);
|
||||
return req->SendOKResponse(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new filter to a source. Available source types along with their settings properties are available from `GetSourceTypesList`.
|
||||
*
|
||||
@ -1339,6 +1378,42 @@ HandlerResponse WSRequestHandler::HandleSetSourceFilterSettings(WSRequestHandler
|
||||
return req->SendOKResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the visibility/enabled state of a filter
|
||||
*
|
||||
* @param {String} `sourceName` Source name
|
||||
* @param {String} `filterName` Source filter name
|
||||
* @param {String} `filterEnabled` New filter state
|
||||
*
|
||||
* @api requests
|
||||
* @name SetSourceFilterVisibility
|
||||
* @category sources
|
||||
* @since 4.7.0
|
||||
*/
|
||||
HandlerResponse WSRequestHandler::HandleSetSourceFilterVisibility(WSRequestHandler* req)
|
||||
{
|
||||
if (!req->hasField("sourceName") || !req->hasField("filterName") || !req->hasField("filterEnabled")) {
|
||||
return req->SendErrorResponse("missing request parameters");
|
||||
}
|
||||
|
||||
const char* sourceName = obs_data_get_string(req->data, "sourceName");
|
||||
OBSSourceAutoRelease source = obs_get_source_by_name(sourceName);
|
||||
if (!source) {
|
||||
return req->SendErrorResponse("specified source doesn't exist");
|
||||
}
|
||||
|
||||
const char* filterName = obs_data_get_string(req->data, "filterName");
|
||||
OBSSourceAutoRelease filter = obs_source_get_filter_by_name(source, filterName);
|
||||
if (!filter) {
|
||||
return req->SendErrorResponse("specified filter doesn't exist on specified source");
|
||||
}
|
||||
|
||||
bool filterEnabled = obs_data_get_bool(req->data, "filterEnabled");
|
||||
obs_source_set_enabled(filter, filterEnabled);
|
||||
|
||||
return req->SendOKResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a picture snapshot of a source and then can either or both:
|
||||
* - Send it over as a Data URI (base64-encoded data) in the response (by specifying `embedPictureFormat` in the request)
|
||||
@ -1349,7 +1424,7 @@ HandlerResponse WSRequestHandler::HandleSetSourceFilterSettings(WSRequestHandler
|
||||
* Clients can specify `width` and `height` parameters to receive scaled pictures. Aspect ratio is
|
||||
* preserved if only one of these two parameters is specified.
|
||||
*
|
||||
* @param {String} `sourceName` Source name
|
||||
* @param {String} `sourceName` Source name. Note that, since scenes are also sources, you can also provide a scene name.
|
||||
* @param {String (optional)} `embedPictureFormat` Format of the Data URI encoded picture. Can be "png", "jpg", "jpeg" or "bmp" (or any other value supported by Qt's Image module)
|
||||
* @param {String (optional)} `saveToFilePath` Full file path (file extension included) where the captured image is to be saved. Can be in a format different from `pictureFormat`. Can be a relative path.
|
||||
* @param {int (optional)} `width` Screenshot width. Defaults to the source's base width.
|
||||
|
@ -26,19 +26,17 @@ HandlerResponse WSRequestHandler::HandleGetStreamingStatus(WSRequestHandler* req
|
||||
OBSDataAutoRelease data = obs_data_create();
|
||||
obs_data_set_bool(data, "streaming", obs_frontend_streaming_active());
|
||||
obs_data_set_bool(data, "recording", obs_frontend_recording_active());
|
||||
obs_data_set_bool(data, "recording-paused", Utils::RecordingPaused());
|
||||
obs_data_set_bool(data, "preview-only", false);
|
||||
|
||||
const char* tc = nullptr;
|
||||
if (obs_frontend_streaming_active()) {
|
||||
tc = events->GetStreamingTimecode();
|
||||
obs_data_set_string(data, "stream-timecode", tc);
|
||||
bfree((void*)tc);
|
||||
QString streamingTimecode = events->getStreamingTimecode();
|
||||
obs_data_set_string(data, "stream-timecode", streamingTimecode.toUtf8().constData());
|
||||
}
|
||||
|
||||
if (obs_frontend_recording_active()) {
|
||||
tc = events->GetRecordingTimecode();
|
||||
obs_data_set_string(data, "rec-timecode", tc);
|
||||
bfree((void*)tc);
|
||||
QString recordingTimecode = events->getRecordingTimecode();
|
||||
obs_data_set_string(data, "rec-timecode", recordingTimecode.toUtf8().constData());
|
||||
}
|
||||
|
||||
return req->SendOKResponse(data);
|
||||
|
@ -69,12 +69,12 @@ HandlerResponse WSRequestHandler::HandleSetPreviewScene(WSRequestHandler* req) {
|
||||
}
|
||||
|
||||
const char* scene_name = obs_data_get_string(req->data, "scene-name");
|
||||
OBSSourceAutoRelease scene = Utils::GetSceneFromNameOrCurrent(scene_name);
|
||||
OBSScene scene = Utils::GetSceneFromNameOrCurrent(scene_name);
|
||||
if (!scene) {
|
||||
return req->SendErrorResponse("specified scene doesn't exist");
|
||||
}
|
||||
|
||||
obs_frontend_set_current_preview_scene(scene);
|
||||
obs_frontend_set_current_preview_scene(obs_scene_get_source(scene));
|
||||
return req->SendOKResponse();
|
||||
}
|
||||
|
||||
@ -116,11 +116,11 @@ HandlerResponse WSRequestHandler::HandleTransitionToProgram(WSRequestHandler* re
|
||||
if (obs_data_has_user_value(transitionInfo, "duration")) {
|
||||
int transitionDuration =
|
||||
obs_data_get_int(transitionInfo, "duration");
|
||||
Utils::SetTransitionDuration(transitionDuration);
|
||||
obs_frontend_set_transition_duration(transitionDuration);
|
||||
}
|
||||
}
|
||||
|
||||
Utils::TransitionToProgram();
|
||||
obs_frontend_preview_program_trigger_transition();
|
||||
return req->SendOKResponse();
|
||||
}
|
||||
|
||||
|
@ -56,7 +56,7 @@ HandlerResponse WSRequestHandler::HandleGetCurrentTransition(WSRequestHandler* r
|
||||
obs_source_get_name(currentTransition));
|
||||
|
||||
if (!obs_transition_fixed(currentTransition))
|
||||
obs_data_set_int(response, "duration", Utils::GetTransitionDuration());
|
||||
obs_data_set_int(response, "duration", obs_frontend_get_transition_duration());
|
||||
|
||||
return req->SendOKResponse(response);
|
||||
}
|
||||
@ -101,7 +101,7 @@ HandlerResponse WSRequestHandler::HandleSetTransitionDuration(WSRequestHandler*
|
||||
}
|
||||
|
||||
int ms = obs_data_get_int(req->data, "duration");
|
||||
Utils::SetTransitionDuration(ms);
|
||||
obs_frontend_set_transition_duration(ms);
|
||||
return req->SendOKResponse();
|
||||
}
|
||||
|
||||
@ -117,6 +117,6 @@ HandlerResponse WSRequestHandler::HandleSetTransitionDuration(WSRequestHandler*
|
||||
*/
|
||||
HandlerResponse WSRequestHandler::HandleGetTransitionDuration(WSRequestHandler* req) {
|
||||
OBSDataAutoRelease response = obs_data_create();
|
||||
obs_data_set_int(response, "transition-duration", Utils::GetTransitionDuration());
|
||||
obs_data_set_int(response, "transition-duration", obs_frontend_get_transition_duration());
|
||||
return req->SendOKResponse(response);
|
||||
}
|
||||
|
@ -134,7 +134,15 @@ void WSServer::broadcast(std::string message)
|
||||
continue;
|
||||
}
|
||||
}
|
||||
_server.send(hdl, message, websocketpp::frame::opcode::text);
|
||||
|
||||
websocketpp::lib::error_code errorCode;
|
||||
_server.send(hdl, message, websocketpp::frame::opcode::text, errorCode);
|
||||
|
||||
if (errorCode) {
|
||||
std::string errorCodeMessage = errorCode.message();
|
||||
blog(LOG_INFO, "server(broadcast): send failed: %s",
|
||||
errorCodeMessage.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -166,7 +174,14 @@ void WSServer::onMessage(connection_hdl hdl, server::message_ptr message)
|
||||
WSRequestHandler handler(connProperties);
|
||||
std::string response = handler.processIncomingMessage(payload);
|
||||
|
||||
_server.send(hdl, response, websocketpp::frame::opcode::text);
|
||||
websocketpp::lib::error_code errorCode;
|
||||
_server.send(hdl, response, websocketpp::frame::opcode::text, errorCode);
|
||||
|
||||
if (errorCode) {
|
||||
std::string errorCodeMessage = errorCode.message();
|
||||
blog(LOG_INFO, "server(response): send failed: %s",
|
||||
errorCodeMessage.c_str());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -33,9 +33,6 @@ with this program. If not, see <https://www.gnu.org/licenses/>
|
||||
|
||||
#include "WSRequestHandler.h"
|
||||
|
||||
QT_FORWARD_DECLARE_CLASS(QWebSocketServer)
|
||||
QT_FORWARD_DECLARE_CLASS(QWebSocket)
|
||||
|
||||
using websocketpp::connection_hdl;
|
||||
|
||||
typedef websocketpp::server<websocketpp::config::asio> server;
|
||||
|
@ -18,6 +18,7 @@ with this program. If not, see <https://www.gnu.org/licenses/>
|
||||
|
||||
#include <obs-module.h>
|
||||
#include <obs-frontend-api.h>
|
||||
#include <obs-data.h>
|
||||
|
||||
#include <QtCore/QTimer>
|
||||
#include <QtWidgets/QAction>
|
||||
@ -35,6 +36,11 @@ void ___data_dummy_addref(obs_data_t*) {}
|
||||
void ___data_array_dummy_addref(obs_data_array_t*) {}
|
||||
void ___output_dummy_addref(obs_output_t*) {}
|
||||
|
||||
void ___data_item_dummy_addref(obs_data_item_t*) {}
|
||||
void ___data_item_release(obs_data_item_t* dataItem) {
|
||||
obs_data_item_release(&dataItem);
|
||||
}
|
||||
|
||||
OBS_DECLARE_MODULE()
|
||||
OBS_MODULE_USE_DEFAULT_LOCALE("obs-websocket", "en-US")
|
||||
|
||||
@ -55,10 +61,6 @@ bool obs_module_load(void) {
|
||||
_server = WSServerPtr(new WSServer());
|
||||
_eventsSystem = WSEventsPtr(new WSEvents(_server));
|
||||
|
||||
if (_config->ServerEnabled) {
|
||||
_server->start(_config->ServerPort);
|
||||
}
|
||||
|
||||
// UI setup
|
||||
obs_frontend_push_ui_translation(obs_module_get_string);
|
||||
QMainWindow* mainWindow = (QMainWindow*)obs_frontend_get_main_window();
|
||||
@ -75,6 +77,17 @@ bool obs_module_load(void) {
|
||||
settingsDialog->ToggleShowHide();
|
||||
});
|
||||
|
||||
// Setup event handler to start the server once OBS is ready
|
||||
auto eventCallback = [](enum obs_frontend_event event, void *param) {
|
||||
if (event == OBS_FRONTEND_EVENT_FINISHED_LOADING) {
|
||||
if (_config->ServerEnabled) {
|
||||
_server->start(_config->ServerPort);
|
||||
}
|
||||
obs_frontend_remove_event_callback((obs_frontend_event_cb)param, nullptr);
|
||||
}
|
||||
};
|
||||
obs_frontend_add_event_callback(eventCallback, (void*)(obs_frontend_event_cb)eventCallback);
|
||||
|
||||
// Loading finished
|
||||
blog(LOG_INFO, "module loaded!");
|
||||
|
||||
|
@ -38,6 +38,11 @@ using OBSDataArrayAutoRelease =
|
||||
using OBSOutputAutoRelease =
|
||||
OBSRef<obs_output_t*, ___output_dummy_addref, obs_output_release>;
|
||||
|
||||
void ___data_item_dummy_addref(obs_data_item_t*);
|
||||
void ___data_item_release(obs_data_item_t*);
|
||||
using OBSDataItemAutoRelease =
|
||||
OBSRef<obs_data_item_t*, ___data_item_dummy_addref, ___data_item_release>;
|
||||
|
||||
class Config;
|
||||
typedef std::shared_ptr<Config> ConfigPtr;
|
||||
|
||||
@ -51,6 +56,6 @@ ConfigPtr GetConfig();
|
||||
WSServerPtr GetServer();
|
||||
WSEventsPtr GetEventsSystem();
|
||||
|
||||
#define OBS_WEBSOCKET_VERSION "4.6.1"
|
||||
#define OBS_WEBSOCKET_VERSION "4.7.0"
|
||||
|
||||
#define blog(level, msg, ...) blog(level, "[obs-websocket] " msg, ##__VA_ARGS__)
|
||||
|
Reference in New Issue
Block a user