Compare commits

..

115 Commits
4.6.0 ... 4.7.0

Author SHA1 Message Date
9a8f248a75 docs(travis): Update protocol.md - 6845cda [skip ci] 2019-11-12 10:41:34 +00:00
6845cda146 Update WSRequestHandler_Sources.cpp 2019-11-12 11:40:36 +01:00
4488b2b7a5 Merge pull request #385 from Palakis/bugfix/macos-deps-lookup
ci(macos): fix lookup path of Qt dependencies for new OBS bundle format
2019-11-11 15:38:34 +01:00
7dd8cb5d8d ci(macos): use framework rpath in package script 2019-11-11 15:27:27 +01:00
ebf631a15f ci(macos): fix lib paths in package script 2019-11-11 14:54:17 +01:00
3770b75a78 docs(travis): Update protocol.md - 5d81b61 [skip ci] 2019-11-11 13:41:29 +00:00
5d81b61325 Merge pull request #388 from Palakis/feature/enable-disable-filters
Get/set filter visibility (+ get single filter info)
2019-11-11 14:40:32 +01:00
a8e5171b40 utils(GetSourceFilterInfo): fix settings memory leak 2019-11-11 14:27:40 +01:00
a3e0abb6ce events: add SourceFilterVisibilityChanged 2019-11-11 14:12:37 +01:00
c51f20eb99 requests(SetSourceFilterVisibility): fix parameter value 2019-11-11 13:43:33 +01:00
be897c4df2 requests(sources): fix typo 2019-11-11 13:42:16 +01:00
11b948fb69 Revert "ci(macos): use @rpath in package script"
This reverts commit 070660848b.
2019-11-11 13:36:04 +01:00
070660848b ci(macos): use @rpath in package script 2019-11-11 13:27:33 +01:00
61af0ec9c6 requests(sources): add SetSourceFilterVisibility method 2019-11-11 12:58:36 +01:00
897f115363 requests(sources): add GetSourceFilterInfo method 2019-11-11 12:53:22 +01:00
b8fcf0355c requests(GetSourceFilters): add "enabled" status field 2019-11-11 12:44:13 +01:00
5bdff87e3f ci(macos): fix lookup path of Qt dependencies for new OBS bundle format 2019-11-04 13:07:15 +01:00
31991a3567 docs(travis): Update protocol.md - 25c369d [skip ci] 2019-10-17 14:19:25 +00:00
25c369d422 Merge pull request #384 from BarryCarlyon/TakeSourceScreenshotDocs
Take scene screenshot
2019-10-17 16:18:16 +02:00
8d396b1518 Adjust wording based on feedback 2019-10-17 11:53:32 +01:00
ba327b746a Wording Tweak 2019-10-15 20:13:00 +01:00
7d11f912c9 Revise Documentation to note that a Scene Name can be provided 2019-10-15 20:11:40 +01:00
919cdfb5a9 WSServer: remove remains of QWebSocket 2019-09-16 13:01:44 +02:00
da9dd6f775 docs(travis): Update protocol.md - 8b841f0 [skip ci] 2019-09-03 19:29:52 +00:00
8b841f026e Merge pull request #371 from Palakis/pause-recording
Recording Pause support
2019-09-03 21:28:55 +02:00
5342b39640 WSEvents: use frame count for streaming timecode 2019-09-03 21:04:45 +02:00
fc08add504 WSEvents: use stream time getter 2019-09-03 16:05:38 +02:00
20379ac61e WSEvents: recording time based on frame count and frame duration 2019-09-03 16:03:52 +02:00
af40aa59ab events + requests: fix dynamic runtime support for recording pauses 2019-09-03 13:50:44 +02:00
f9afc5597a requests(pause recording): fix memory leak 2019-09-03 13:22:08 +02:00
40178e4661 WSEvents(nsToTimestamp): fix sprintf compiler warning 2019-09-03 09:56:20 +02:00
ad56abd6f5 requests(Recording): refactor bits 2019-09-03 03:34:20 +02:00
9073a09d84 requests(pause recording): runtime feature detection 2019-09-03 03:30:06 +02:00
2a4e6fce3f WSEvents: fix constructor warning 2019-09-03 03:03:06 +02:00
0a3a407217 events: refactor timecode generation 2019-09-03 02:59:02 +02:00
9ca9cf80df Merge branch '4.x-current' into pause-recording 2019-09-03 02:41:42 +02:00
6c881a5da7 Merge pull request #372 from Palakis/frontend-events-ifelse-refactor
events: refactor the massive if else chain in the frontend event callback
2019-09-03 02:40:07 +02:00
5322acd58c events: add RecordingPaused and RecordingResumed 2019-09-03 02:35:54 +02:00
ce489e53c6 requests(GetStreamingStatus): add property "recording-paused" 2019-09-03 02:34:24 +02:00
0c95e509dc events(Heartbeat + StreamStatus): add property "recording-paused" 2019-09-03 02:34:24 +02:00
46a006ed54 requests(pause recording): only compile recording pause function bodies when supported 2019-09-03 02:34:24 +02:00
0cc9378d05 requests: add PauseRecording and ResumeRecording 2019-09-03 02:34:24 +02:00
f3edb2ee68 events: refactor the massive if else chain in the frontend event callback 2019-09-03 02:34:11 +02:00
a539f23194 Update README.md 2019-09-02 17:53:18 +02:00
6b6dec79c5 Merge pull request #369 from Palakis/bugfix/connection-onclose-broadcast-crash
WSServer: handle error codes to avoid throwing exceptions when sending a message
2019-09-02 12:52:58 +02:00
d7f5c042bf WSServer: handle error codes when sending a message 2019-09-01 00:16:09 +02:00
2fe9812b26 gitignore: .idea folder 2019-09-01 00:04:31 +02:00
1c15c3c7a3 Merge pull request #368 from Palakis/bugfix/timecode-sprintf-warning
WSEvents(nsToTimestamp): fix sprintf format warning
2019-08-31 21:50:46 +02:00
1ee5857139 WSEvents(nsToTimestamp): use unsigned long long constants 2019-08-31 21:35:58 +02:00
2f9c2b0d65 WSEvents(nsToTimestamp): fix sprintf format warning 2019-08-31 18:34:49 +02:00
6e5161f43b Merge pull request #367 from Palakis/bugfix/start-server-after-loading-finished
Start server after OBS finishes loading
2019-08-31 17:39:08 +02:00
d492d74a64 module: start server after OBS finishes loading 2019-08-31 16:03:32 +02:00
26506731c5 Merge pull request #366 from Palakis/bugfix/incorrect-recording-free-space
events(GetStats): fix recording path detection
2019-08-31 04:50:40 +02:00
b0c03d4f47 events(GetStats): fix recording path detection 2019-08-31 04:31:40 +02:00
3bc8348c32 Merge pull request #365 from Palakis/bugfix/reordersceneitems-crash
ReorderSceneItems: fix crash + refactor Utils
2019-08-31 03:29:21 +02:00
a034fbba83 utils(GetSceneItemFromItem): fix memory leaks with obs_data_item_t uses 2019-08-31 03:10:49 +02:00
73b5261c49 requests(ReorderSceneItems): atomic reorder + bring back GetSceneItemFromId 2019-08-31 03:03:32 +02:00
cee0cbebc2 Utils: use obs_scene_t* directly for scene-related helpers 2019-08-31 03:03:32 +02:00
9723147429 Utils: refactor GetSceneItemFromItem and get rid of GetSceneItemFromId 2019-08-31 03:03:32 +02:00
dca385ae87 requests(ReorderSceneItems): refactor 2019-08-31 03:03:32 +02:00
71f792944c request handler: fix obs_data_item_t memory leak 2019-08-31 02:52:08 +02:00
6041c4acd2 Merge pull request #364 from Palakis/bugfix/transitionbegin-cut-duration
event(TransitionBegin): set duration to 0 if it's a Cut transition
2019-08-30 14:10:46 +02:00
0c7529705e docs(travis): Update protocol.md - 63f1ea1 [skip ci] 2019-08-30 11:54:21 +00:00
63f1ea1cec docs: bump docs version to 4.7.0 2019-08-30 13:53:11 +02:00
4bbaa75f41 Merge pull request #358 from Palakis/request-helpers
Extend WSRequestHandler::hasField() to support field type checking
2019-08-30 13:50:25 +02:00
5fe9314b74 WSEvents: remove unused transitionIsCut helper 2019-08-30 13:41:50 +02:00
bfd26493ab Utils(GetTransitionDuration): return 0 if transition is a cut transition 2019-08-30 13:41:08 +02:00
5d74d5d03e WSEvents(OnTransitionBegin): refactor event code 2019-08-30 13:36:45 +02:00
3142e097a5 Merge pull request #360 from ZyanKLee/patch-1
allow for "linuxbrowser_source" to be controlled
2019-08-26 21:02:00 +02:00
b25d8d8201 requests(sources): fix linux browser source id 2019-08-26 21:01:05 +02:00
7e68cd016a ci(windows): exclude RCs when fetching the latest version of OBS 2019-08-24 12:39:46 +02:00
9e4fc9eb73 allow for "linuxbrowser_source" to be controlled
based on the request at https://github.com/bazukas/obs-linuxbrowser/issues/46 I did a quick search for the string "browser_source" and these seem to be the only two occurrences. Enhancing the conditional with the identifier of the obs-linuxbrowser plugin, this should solve the problem.

As I'm currently not at home, I can't verify this does what it should, but I will test it later tonight.

@mjc666 if you are still interested please give it a test run to verify if these two plugins work together now.
2019-08-22 19:44:12 +02:00
fb0d13a171 WSRequestHandler: add int and double type check methods 2019-08-02 21:52:50 +02:00
f097b36c2e WSRequestHandler: add type checks for field test methods 2019-08-02 21:40:42 +02:00
57ce3cf80b Merge pull request #320 from Palakis/feature/remove-hacks
utils: remove transition hacks
2019-07-31 17:38:40 +02:00
c1da62391f docs(travis): Update protocol.md - d8882ce [skip ci] 2019-07-31 15:37:41 +00:00
d8882ce979 Merge pull request #357 from julijane/fix-001
Fix spec of HandleBroadcastCustomMessage
2019-07-31 17:36:39 +02:00
9c1871494e Fix spec of HandleBroadcastCustomMessage 2019-07-26 14:40:54 +02:00
ac26c9114a docs(travis): Update protocol.md - fb0a570 [skip ci] 2019-07-26 07:31:42 +00:00
fb0a57003c BroadcastCustomMessage: fix command and event spec 2019-07-26 09:30:30 +02:00
cda22233a6 docs(travis): Update protocol.md - add3075 [skip ci] 2019-07-26 03:52:35 +00:00
add307577d Merge pull request #354 from julijane/4.x-current
Make it possible to broadcast messages (data) to other websocket clients
2019-07-26 05:51:42 +02:00
4af46ac2b9 use obs_data_t* in Event function 2019-07-25 17:20:31 +02:00
8d39752bda Move event emitting code 2019-07-25 17:00:32 +02:00
e3ff9c013e change function name, language 2019-07-25 14:47:47 +02:00
635870ba4b more comment fixes 2019-07-24 05:36:47 +02:00
a827afb05b Comment fix 2019-07-24 01:56:53 +02:00
0195e13bdc Implement proposed changes to Broadcast 2019-07-23 21:47:55 +02:00
76d8b688fd Add Code for BroadcastWebSocketMessage 2019-07-22 01:05:21 +02:00
b4857e2c79 docs(travis): Update protocol.md - ac2ae90 [skip ci] 2019-07-16 16:23:36 +00:00
ac2ae90e8a Merge pull request #343 from Palakis/feature/outputs-api
Outputs API
2019-07-16 18:22:40 +02:00
0b5cb76b5d outputs: remove TODOs 2019-07-16 18:22:07 +02:00
6eed9606ae docs: remove Boost from BUILDING.md 2019-06-23 22:38:36 +02:00
29679ff94c docs(travis): Update protocol.md - 65a1f8b [skip ci] 2019-06-18 16:41:13 +00:00
65a1f8b746 GetSourceTypesList: fix docs
Fixes #348
2019-06-18 18:38:52 +02:00
48182d35bd docs(travis): Update protocol.md - b526ed4 [skip ci] 2019-06-16 14:20:37 +00:00
b526ed47be Merge branch '4.x-current' of github.com:Palakis/obs-websocket into 4.x-current 2019-06-16 16:19:32 +02:00
2225dec2a9 docs(SourceAudioMixersChanged): fix remaining discrepancy 2019-06-16 16:19:22 +02:00
011458d642 docs(travis): Update protocol.md - e32fff3 [skip ci] 2019-06-16 14:18:12 +00:00
e32fff3f61 docs(SourceAudioMixersChanged): fix discrepancy 2019-06-16 16:14:26 +02:00
0b42e3d0f3 outputs: add more info fields 2019-06-11 19:32:52 +02:00
2b746d1353 cmake: add output requests 2019-06-11 19:32:52 +02:00
6fed6f9f7d StartOutput: provide output error message when start fails 2019-06-11 19:32:52 +02:00
480945073a outputs: start or stop only when possible 2019-06-11 19:32:52 +02:00
ef6df94838 outputs: todos 2019-06-11 19:32:52 +02:00
fb05848426 requests: outputs API WIP 2019-06-11 19:32:52 +02:00
e5be6b96fb general: version bump to 4.7 2019-06-11 19:32:32 +02:00
2d706a245a Merge pull request #337 from DieserMerlin/patch-2
Update README.md (+ obs-websocket-java library)
2019-06-04 11:17:43 +02:00
1abc4f491b readme: minor fix 2019-06-04 10:46:19 +02:00
b21a45b43e Update README.md 2019-06-01 00:08:19 +02:00
585f8450e1 Merge pull request #330 from Palakis/transitionbegin-hotfix
Fix TransitionBegin event
2019-05-21 00:05:24 +02:00
d3401db5fe general: version bump to 4.6.1 2019-05-21 00:02:48 +02:00
45dc6447bf events: fix how and when TransitionBegin is hooked 2019-05-20 23:54:13 +02:00
409c270ba2 events: fix TransitionBegin event not triggering 2019-05-20 23:48:49 +02:00
84629f6e63 utils: remove transition hacks 2019-05-09 13:28:50 +02:00
30 changed files with 2142 additions and 436 deletions

4
.gitignore vendored
View File

@ -5,5 +5,5 @@
/build64/
/release/
/installer/Output/
.vscode
.idea
.vscode

View File

@ -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

View File

@ -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
)

View File

@ -518,7 +518,7 @@
<key>OVERWRITE_PERMISSIONS</key>
<false/>
<key>VERSION</key>
<string>4.6.0</string>
<string>4.7.0</string>
</dict>
<key>PROJECT_COMMENTS</key>
<dict>

View File

@ -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 \

View File

@ -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

View File

@ -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)
[![Build Status - Windows](https://ci.appveyor.com/api/projects/status/github/Palakis/obs-websocket)](https://ci.appveyor.com/project/Palakis/obs-websocket/history) [![Build Status - Linux & OS X](https://travis-ci.org/Palakis/obs-websocket.svg?branch=master)](https://travis-ci.org/Palakis/obs-websocket)
[![Build Status - Windows](https://ci.appveyor.com/api/projects/status/github/Palakis/obs-websocket)](https://ci.appveyor.com/project/Palakis/obs-websocket/history) [![Build Status - Linux](https://travis-ci.org/Palakis/obs-websocket.svg?branch=master)](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:

View File

@ -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)",

View File

@ -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&lt;Object&gt;_ | 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&lt;Object&gt;_ | 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&lt;Output&gt;_ | 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&lt;Object&gt;_ | 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&lt;Object&gt;_ | 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&lt;Object&gt;_ | 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. |

View File

@ -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.

View File

@ -2,7 +2,7 @@
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
#define MyAppName "obs-websocket"
#define MyAppVersion "4.6.0"
#define MyAppVersion "4.7.0"
#define MyAppPublisher "Stephane Lepin"
#define MyAppURL "http://github.com/Palakis/obs-websocket"

View File

@ -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")) {
return nullptr;
}
}
else if (obs_data_has_user_value(item, "name")) {
sceneItem = GetSceneItemFromName(source, obs_data_get_string(item, "name"));
}
return sceneItem;
obs_sceneitem_t* Utils::GetSceneItemFromItem(obs_scene_t* scene, obs_data_t* itemInfo) {
if (!scene) {
return nullptr;
}
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_source_t* source, QString name) {
obs_sceneitem_t* Utils::GetSceneItemFromName(obs_scene_t* scene, QString name) {
if (!scene) {
return nullptr;
}
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,21 +263,16 @@ 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,
obs_sceneitem_t* currentItem,
void* param)
obs_scene_t* scene,
obs_sceneitem_t* currentItem,
void* param)
{
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);
}

View File

@ -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);
};

View File

@ -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,81 +132,120 @@ void WSEvents::FrontendEventHandler(enum obs_frontend_event event, void* private
return;
}
if (event == OBS_FRONTEND_EVENT_SCENE_CHANGED) {
owner->OnSceneChange();
}
else if (event == OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED) {
owner->OnSceneListChange();
}
else if (event == OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED) {
owner->OnSceneCollectionChange();
}
else if (event == OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED) {
owner->OnSceneCollectionListChange();
}
else if (event == OBS_FRONTEND_EVENT_TRANSITION_CHANGED) {
owner->OnTransitionChange();
}
else if (event == OBS_FRONTEND_EVENT_TRANSITION_LIST_CHANGED) {
owner->OnTransitionListChange();
}
else if (event == OBS_FRONTEND_EVENT_PROFILE_CHANGED) {
owner->OnProfileChange();
}
else if (event == OBS_FRONTEND_EVENT_PROFILE_LIST_CHANGED) {
owner->OnProfileListChange();
}
else if (event == OBS_FRONTEND_EVENT_STREAMING_STARTING) {
owner->OnStreamStarting();
}
else if (event == OBS_FRONTEND_EVENT_STREAMING_STARTED) {
owner->streamStatusTimer.start(STATUS_INTERVAL);
owner->StreamStatus();
switch (event) {
case OBS_FRONTEND_EVENT_FINISHED_LOADING:
owner->hookTransitionBeginEvent();
break;
case OBS_FRONTEND_EVENT_SCENE_CHANGED:
owner->OnSceneChange();
break;
owner->OnStreamStarted();
}
else if (event == OBS_FRONTEND_EVENT_STREAMING_STOPPING) {
owner->streamStatusTimer.stop();
owner->OnStreamStopping();
}
else if (event == OBS_FRONTEND_EVENT_STREAMING_STOPPED) {
owner->OnStreamStopped();
}
else if (event == OBS_FRONTEND_EVENT_RECORDING_STARTING) {
owner->OnRecordingStarting();
}
else if (event == OBS_FRONTEND_EVENT_RECORDING_STARTED) {
owner->OnRecordingStarted();
}
else if (event == OBS_FRONTEND_EVENT_RECORDING_STOPPING) {
owner->OnRecordingStopping();
}
else if (event == OBS_FRONTEND_EVENT_RECORDING_STOPPED) {
owner->OnRecordingStopped();
}
else if (event == OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING) {
owner->OnReplayStarting();
}
else if (event == OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED) {
owner->OnReplayStarted();
}
else if (event == OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING) {
owner->OnReplayStopping();
}
else if (event == OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED) {
owner->OnReplayStopped();
}
else if (event == OBS_FRONTEND_EVENT_STUDIO_MODE_ENABLED) {
owner->OnStudioModeSwitched(true);
}
else if (event == OBS_FRONTEND_EVENT_STUDIO_MODE_DISABLED) {
owner->OnStudioModeSwitched(false);
}
else if (event == OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED) {
owner->OnPreviewSceneChanged();
}
else if (event == OBS_FRONTEND_EVENT_EXIT) {
owner->OnExit();
case OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED:
owner->OnSceneListChange();
break;
case OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED:
owner->hookTransitionBeginEvent();
owner->OnSceneCollectionChange();
break;
case OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED:
owner->OnSceneCollectionListChange();
break;
case OBS_FRONTEND_EVENT_TRANSITION_CHANGED:
owner->OnTransitionChange();
break;
case OBS_FRONTEND_EVENT_TRANSITION_LIST_CHANGED:
owner->hookTransitionBeginEvent();
owner->OnTransitionListChange();
break;
case OBS_FRONTEND_EVENT_PROFILE_CHANGED:
owner->OnProfileChange();
break;
case OBS_FRONTEND_EVENT_PROFILE_LIST_CHANGED:
owner->OnProfileListChange();
break;
case OBS_FRONTEND_EVENT_STREAMING_STARTING:
owner->OnStreamStarting();
break;
case OBS_FRONTEND_EVENT_STREAMING_STARTED:
owner->streamStatusTimer.start(STATUS_INTERVAL);
owner->StreamStatus();
owner->OnStreamStarted();
break;
case OBS_FRONTEND_EVENT_STREAMING_STOPPING:
owner->streamStatusTimer.stop();
owner->OnStreamStopping();
break;
case OBS_FRONTEND_EVENT_STREAMING_STOPPED:
owner->OnStreamStopped();
break;
case OBS_FRONTEND_EVENT_RECORDING_STARTING:
owner->OnRecordingStarting();
break;
case OBS_FRONTEND_EVENT_RECORDING_STARTED:
owner->OnRecordingStarted();
break;
case OBS_FRONTEND_EVENT_RECORDING_STOPPING:
owner->OnRecordingStopping();
break;
case OBS_FRONTEND_EVENT_RECORDING_STOPPED:
owner->OnRecordingStopped();
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();
break;
case OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED:
owner->OnReplayStarted();
break;
case OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING:
owner->OnReplayStopping();
break;
case OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED:
owner->OnReplayStopped();
break;
case OBS_FRONTEND_EVENT_STUDIO_MODE_ENABLED:
owner->OnStudioModeSwitched(true);
break;
case OBS_FRONTEND_EVENT_STUDIO_MODE_DISABLED:
owner->OnStudioModeSwitched(false);
break;
case OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED:
owner->OnPreviewSceneChanged();
break;
case OBS_FRONTEND_EVENT_EXIT:
owner->unhookTransitionBeginEvent();
owner->OnExit();
break;
}
}
@ -227,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)
@ -283,9 +308,6 @@ void WSEvents::connectSourceSignals(obs_source_t* source) {
signal_handler_connect(sh, "item_select", OnSceneItemSelected, this);
signal_handler_connect(sh, "item_deselect", OnSceneItemDeselected, this);
}
if (sourceType == OBS_SOURCE_TYPE_TRANSITION) {
signal_handler_connect(sh, "transition_start", OnTransitionBegin, this);
}
}
void WSEvents::disconnectSourceSignals(obs_source_t* source) {
@ -318,26 +340,81 @@ void WSEvents::disconnectSourceSignals(obs_source_t* source) {
signal_handler_disconnect(sh, "transition_start", OnTransitionBegin, this);
}
uint64_t WSEvents::GetStreamingTime() {
if (obs_frontend_streaming_active())
return (os_gettime_ns() - _streamStarttime);
else
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);
for (int i = 0; i < transitions.sources.num; i++) {
obs_source_t* transition = transitions.sources.array[i];
signal_handler_t* sh = obs_source_get_signal_handler(transition);
signal_handler_disconnect(sh, "transition_start", OnTransitionBegin, this);
signal_handler_connect(sh, "transition_start", OnTransitionBegin, this);
}
obs_frontend_source_list_free(&transitions);
}
void WSEvents::unhookTransitionBeginEvent() {
obs_frontend_source_list transitions = {};
obs_frontend_get_transitions(&transitions);
for (int i = 0; i < transitions.sources.num; i++) {
obs_source_t* transition = transitions.sources.array[i];
signal_handler_t* sh = obs_source_get_signal_handler(transition);
signal_handler_disconnect(sh, "transition_start", OnTransitionBegin, this);
}
obs_frontend_source_list_free(&transitions);
}
uint64_t getOutputRunningTime(obs_output_t* output) {
if (!output || !obs_output_active(output)) {
return 0;
}
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);
}
const char* WSEvents::GetStreamingTimecode() {
return nsToTimestamp(GetStreamingTime());
uint64_t WSEvents::getStreamingTime() {
OBSOutputAutoRelease streamingOutput = obs_frontend_get_streaming_output();
return getOutputRunningTime(streamingOutput);
}
uint64_t WSEvents::GetRecordingTime() {
if (obs_frontend_recording_active())
return (os_gettime_ns() - _recStarttime);
else
return 0;
uint64_t WSEvents::getRecordingTime() {
OBSOutputAutoRelease recordingOutput = obs_frontend_get_recording_output();
return getOutputRunningTime(recordingOutput);
}
const char* WSEvents::GetRecordingTimecode() {
return nsToTimestamp(GetRecordingTime());
QString WSEvents::getStreamingTimecode() {
return nsToTimestamp(getStreamingTime());
}
QString WSEvents::getRecordingTimecode() {
return nsToTimestamp(getRecordingTime());
}
/**
@ -490,6 +567,7 @@ void WSEvents::OnStreamStarting() {
void WSEvents::OnStreamStarted() {
_streamStarttime = os_gettime_ns();
_lastBytesSent = 0;
broadcastUpdate("StreamStarted");
}
@ -506,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);
}
@ -520,6 +597,7 @@ void WSEvents::OnStreamStopping() {
*/
void WSEvents::OnStreamStopped() {
_streamStarttime = 0;
broadcastUpdate("StreamStopped");
}
@ -544,7 +622,6 @@ void WSEvents::OnRecordingStarting() {
* @since 0.3
*/
void WSEvents::OnRecordingStarted() {
_recStarttime = os_gettime_ns();
broadcastUpdate("RecordingStarted");
}
@ -569,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.
*
@ -664,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();
@ -690,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);
@ -702,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);
@ -748,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();
@ -763,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));
}
@ -817,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 !");
}
obs_data_set_int(fields, "duration", duration);
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));
}
@ -1011,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
@ -1108,6 +1200,8 @@ void WSEvents::OnSourceFilterAdded(void* param, calldata_t* data) {
if (!filter) {
return;
}
self->connectFilterSignals(filter);
OBSDataAutoRelease filterSettings = obs_source_get_settings(filter);
@ -1140,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));
@ -1148,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.
*
@ -1475,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.
@ -1500,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());

View File

@ -40,12 +40,22 @@ public:
void connectSourceSignals(obs_source_t* source);
void disconnectSourceSignals(obs_source_t* source);
uint64_t GetStreamingTime();
const char* GetStreamingTimecode();
uint64_t GetRecordingTime();
const char* GetRecordingTimecode();
void connectFilterSignals(obs_source_t* filter);
void disconnectFilterSignals(obs_source_t* filter);
void hookTransitionBeginEvent();
void unhookTransitionBeginEvent();
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:
@ -62,7 +72,6 @@ private:
bool pulse;
uint64_t _streamStarttime;
uint64_t _recStarttime;
uint64_t _lastBytesSent;
uint64_t _lastBytesSentTime;
@ -90,6 +99,8 @@ private:
void OnRecordingStarted();
void OnRecordingStopping();
void OnRecordingStopped();
void OnRecordingPaused();
void OnRecordingResumed();
void OnReplayStarting();
void OnReplayStarted();
@ -118,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);

View File

@ -24,7 +24,7 @@
#include "WSRequestHandler.h"
QHash<QString, HandlerResponse(*)(WSRequestHandler*)> WSRequestHandler::messageMap {
QHash<QString, HandlerResponse(*)(WSRequestHandler*)> WSRequestHandler::messageMap{
{ "GetVersion", WSRequestHandler::HandleGetVersion },
{ "GetAuthRequired", WSRequestHandler::HandleGetAuthRequired },
{ "Authenticate", WSRequestHandler::HandleAuthenticate },
@ -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);
}

View File

@ -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);
};

View File

@ -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
*

View 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();
});
}

View File

@ -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

View File

@ -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);
}

View File

@ -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);
OBSSceneItemAutoRelease sceneItem = Utils::GetSceneItemFromItem(scene, item);
obs_sceneitem_release(sceneItem); // ref dec
QVector<struct obs_sceneitem_order_info> orderList;
struct obs_sceneitem_order_info info;
if (!sceneItem) {
return req->SendErrorResponse("Invalid sceneItem id or name specified");
}
for (size_t j = 0; j <= i; ++j) {
if (sceneItem == newOrder[j]) {
return req->SendErrorResponse("Duplicate sceneItem in specified order");
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);
if (!sceneItem) {
ctx->success = false;
ctx->errorMessage = "Invalid sceneItem id or name specified";
return;
}
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();

View File

@ -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.

View File

@ -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);

View File

@ -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();
}

View File

@ -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);
}

View File

@ -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());
}
});
}

View File

@ -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;

View File

@ -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!");

View File

@ -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.0"
#define OBS_WEBSOCKET_VERSION "4.7.0"
#define blog(level, msg, ...) blog(level, "[obs-websocket] " msg, ##__VA_ARGS__)