Compare commits

..

330 Commits

Author SHA1 Message Date
b02a32ce06 ci: Apply version suffix to macOS builds 2022-03-01 15:26:45 -08:00
ce31ed177d base: Stuff for beta1 2022-03-01 14:50:12 -08:00
71a32c981c utils: Reserve vector capacity where possible
Slight optimization for iteration
2022-03-01 14:48:10 -08:00
4440327141 docs(ci): Update generated docs - bbf9c28 [skip ci] 2022-03-01 22:37:04 +00:00
bbf9c283c0 eventhandler: Add SceneTransitionVideoEnded 2022-03-01 14:36:35 -08:00
9ac7c5890e eventhandler: Add SceneTransitionEnded 2022-03-01 14:36:35 -08:00
a7698a732f eventhandler: Add SceneTransitionStarted + cleanup
This commit cleans up a bunch of code in the event handler,
making it much easier to understand (IMO). I feel much better
about how we handle connecting and disconnecting callbacks. Before,
we were actually allowing a bunch of callbacks to stay connected and
get cleaned up. Now, we actually properly disconnect them.
2022-03-01 14:36:35 -08:00
e15b2edb4f Merge pull request #917 from you-win/readme/add-godot-obs-websocket-gd
README: Add Godot obs-websocket-gd to library list
2022-02-23 16:25:47 -08:00
7fade98407 README: Add Godot obs-websocket-gd to library list 2022-02-23 19:20:07 -05:00
e0057b05db docs(ci): Update generated docs - aa13828 [skip ci] 2022-02-16 22:26:10 +00:00
aa13828cf5 requesthandler: Add SetSourceFilterName 2022-02-16 13:37:18 -08:00
db2ffa569a Base: Fix some formatting 2022-02-16 13:30:10 -08:00
66c14dced5 requesthandler: Reorder a filter request 2022-02-16 13:23:49 -08:00
29b2b1bd5d docs(ci): Update generated docs - 361547a [skip ci] 2022-02-16 21:17:24 +00:00
361547a96d requesthandler: Filter requests & events (#888)
* Implement filter requests

* Fix CreateSourceFilter

* Implement most Filter events

* build against 27.1.3

* Update main.yml

* SourceFilterNameChanged rename

* revert main.yml changes

* rename SourceFilterCreated and revert CI changes

* cleanup

* Base: Various cleanups + fix -Werror

* Base: A few nitpicks/fixes

* requesthandler: Fix CreateSourceFilter

* utils: Fix CreateSourceFilter

Use obs_source_t* instead of OBSSourceAutoRelease to prevent double
release

* requesthandler: Remove filterIndex from CreateSourceFilter

The purpose of sceneItemEnabled in CreateSceneItem is to hide the
scene item while we still hold the scene mutex (guaranteeing the input
will never be shown). Since we don't hold a mutex when creating
filters, there's no reason to do any extra steps.

* requesthandler: Validate input/filter kinds in *DefaultSettings

Co-authored-by: tt2468 <tt2468@gmail.com>
2022-02-16 13:17:06 -08:00
b3a5c55bef CI: Only codesign when not PR 2022-02-16 12:00:01 -08:00
f76de69b34 CI: Use windows-2019 explicitly
Github actions recently migrated windows-latest to windows-2022 and
in the process broke a bunch of shit.
2022-02-16 11:55:57 -08:00
0b294734a2 docs(ci): Update generated docs - 7b52d7e [skip ci] 2022-02-15 05:02:03 +00:00
7b52d7e015 requesthandler: Move GetRecordDirectory to config
More consistency
2022-02-14 17:11:07 -08:00
9664f28483 requesthandler: Finish transition requests 2022-02-14 17:01:44 -08:00
d9070f9edb requesthandler: Add scene scene transition override requests
It's named like:
`Get Scene (Scene Transition) Override`
2022-02-14 15:57:33 -08:00
559212682a docs(ci): Update generated docs - fa8a091 [skip ci] 2022-02-13 23:20:38 +00:00
fa8a091a3e RequestHandler: Add SendStreamCaption 2022-02-13 15:20:19 -08:00
ab137ce8a4 ci: restrict push builds to the master branch 2022-02-14 00:19:19 +01:00
5a3bed7d8b ci(github): add macOS variant 2022-02-14 00:17:09 +01:00
3362d3f998 ci(macos): bump Packages version 2022-02-13 23:51:39 +01:00
7ca8140a34 ci(macos): use a common password for keychain import steps 2022-02-13 23:47:32 +01:00
341259e610 RequestHandler: Save config after profile parameter change
Fixes a bug where changed parameters were not applying across loads

Fixes #895
2022-02-13 14:46:49 -08:00
c64e360c2d Merge pull request #904 from dnaka91/fix-int-type
server: Fix int type of batch execution enum
2022-02-13 14:41:45 -08:00
7c35d6e738 docs(ci): Update generated docs - b206321 [skip ci] 2022-02-13 22:41:10 +00:00
b206321b05 Merge pull request #903 from dnaka91/fix-field-name-docs
docs: Fix wrong field name in `SceneItemLockStateChanged`
2022-02-13 14:40:49 -08:00
403c69463a server: Fix int type of batch execution enum 2022-02-02 10:36:59 +09:00
eeb7bac4b7 ci(macos): import installer certificate in existing keychain 2022-02-01 10:19:36 +01:00
7113055218 ci(macos): configure productsign with installer certificate 2022-02-01 10:12:42 +01:00
ddf752fd03 docs: Fix wrong field name in SceneItemLockStateChanged 2022-01-29 23:50:12 +09:00
e80bcad1e1 docs(ci): Update generated docs - d2ddde3 [skip ci] 2022-01-29 00:56:54 +00:00
d2ddde3229 eventhandler: Add a few transition events 2022-01-28 16:56:21 -08:00
931a1630ce README: Update link to workflow 2022-01-28 16:07:23 -08:00
5cbc1019ff docs(ci): Update generated docs - 1422723 [skip ci] 2022-01-28 23:56:23 +00:00
14227237d7 Base: [BREAKING] Update default WebSocket port to 4455
Our original strategy of relying on clients to simply detect the
protocol version and use the correct one was optimistic at best,
and it has been realized during the transition process from 4.x to 5.x
that sharing 4444 is not practical. As such, we'll be using 4455 in
the future for 5.x.

If you are a client developer, we suggest continuing to maintain
appropriate protocol version detection and support, as the WebSocket
port is at the end of the day simply a suggestion.
2022-01-28 15:38:08 -08:00
3e2984fd7a eventhandler: Add SceneItemSelected event
So I didn't think anyone actually used this, but I was wrong. So I'm
adding it again.
2022-01-28 15:33:28 -08:00
96a2fd8c25 docs(ci): Update generated docs - 38d7859 [skip ci] 2022-01-27 05:34:40 +00:00
38d78596ce requesthandler: Add replay buffer requests 2022-01-26 21:19:10 -08:00
13c7b83c34 requesthandler: Fix compiler warnings with latest OBS master
OBS has deprecated the `_addref` functions, so the new norm is to use
`_get_ref`.
2022-01-26 17:40:45 -08:00
1844f85e1f CI: Add MacOS CI
Add MacOS CI for `master`

Co-authored-by: tt2468 <tt2468@gmail.com>
2022-01-18 19:26:42 -08:00
bc0b499944 docs(ci): Update generated docs - ae906bb [skip ci] 2022-01-19 03:24:00 +00:00
ae906bb283 RequestHandler: Add VirtualCam requests 2022-01-18 19:23:06 -08:00
63dfed1cf9 docs(ci): Update generated docs - 873eade [skip ci] 2022-01-09 06:14:13 +00:00
873eadec05 requesthandler: Fix documentation of dB value input
Max dB value is 26dB, not -26dB.
2022-01-08 22:13:53 -08:00
dea0fcd561 Base: Add logging for compile time ASIO version 2022-01-07 23:00:48 -08:00
9f7beb1c0d deps: Downgrade asio to 1.12.1
Even though we statically link ASIO, it has issues with the
kqueue_reactor() on macos segfaulting when other plugins using
different ASIO versions are installed. So that means we're stuck using
1.12.1 until we can find some kind of fix for the crash issue.
2022-01-04 00:42:09 -08:00
db9f4b24df docs(ci): Update generated docs - 6035294 [skip ci] 2022-01-03 21:54:54 +00:00
6035294339 requesthandler: Add GetSourceFilter 2022-01-03 13:54:27 -08:00
6a2d5968ad requesthandler: Add private source settings get/set requests
It was requested via Discord to be able to modify the private settings
of any private source, since that functionality is used by some client
software to store stateful data. As private settings are in territory
that no normal user should ever tread into, these requests will be left
undocumented.
2022-01-01 17:43:26 -08:00
8f2d266dec docs(ci): Update generated docs - fe64620 [skip ci] 2022-01-01 02:06:35 +00:00
fe64620731 requesthandler: Add scene item blend mode requests 2021-12-31 18:05:05 -08:00
24e43d0276 requesthandler: Add GetSpecialInputs 2021-12-31 16:49:18 -08:00
c0308d6ce1 docs(ci): Update generated docs - 506a916 [skip ci] 2021-12-31 23:27:15 +00:00
506a9167c3 requesthandler: Add SetInputAudioTracks 2021-12-31 15:26:54 -08:00
043444cad5 workflows: Enable plugin tests on nightly linux builds 2021-12-31 15:26:54 -08:00
35c8a87def requesthandler: Profile requests if PLUGIN_TESTS is enabled 2021-12-31 15:26:54 -08:00
e451a8d6b0 requesthandler: Use unordered_map for request table
Shaves like 0.0005ms off of request time, but still worth noting.
2021-12-31 15:26:54 -08:00
02bcc0ac1b docs(ci): Update generated docs - 702f88c [skip ci] 2021-12-31 22:08:43 +00:00
702f88cea8 requesthandler: Add GetInputAudioTracks 2021-12-31 14:08:22 -08:00
6d216e0412 docs: Fix docs of InputAudioTracksChanged 2021-12-31 14:08:22 -08:00
e6761cf286 docs(ci): Update generated docs - 00dd8d7 [skip ci] 2021-12-30 09:19:59 +00:00
00dd8d7821 lib: Add version define 2021-12-30 01:15:54 -08:00
e43ebde794 Base: Use static_cast in place of reinterpret_cast
static_cast is a much safer cast method
2021-12-30 00:21:29 -08:00
4a2654d095 RequestHandler: Add GetGroupList 2021-12-30 00:12:41 -08:00
6291cb1532 docs(ci): Update generated docs - a90dafb [skip ci] 2021-12-30 05:12:42 +00:00
a90dafb971 Merge pull request #885 from obsproject/feature/input-audio-requests-events
Input audio requests and events
2021-12-29 21:12:23 -08:00
3a96b585ce docs(ci): Update generated docs - 12c6527 [skip ci] 2021-12-30 05:09:00 +00:00
12c6527442 Merge pull request #884 from obsproject/feature/ui-dialog-requests
RequestHandler: Add input open dialog requests
2021-12-29 21:08:41 -08:00
31997db509 EventHandler: Uncomment audio_monitoring signal 2021-12-29 21:05:28 -08:00
85f65952bd Base: Update issue template 2021-12-29 21:04:54 -08:00
9113ff9021 RequestHandler: Add audio balance requests 2021-12-29 21:03:16 -08:00
1ed095de48 EventHandler: Add InputAudioBalanceChanged 2021-12-29 21:03:16 -08:00
a94ac24027 RequestHandler: Add input open dialog requests
Adds
- `OpenInputPropertiesDialog`
- `OpenInputFiltersDialog`
- `OpenInputInteractDialog`
2021-12-29 21:00:11 -08:00
903b7d4171 Merge pull request #860 from obsproject/fix/audio_monitoring_check
Requests: Add support check for monitoring in `SetInputAudioMonitoringType`
2021-12-29 20:59:38 -08:00
a59ce69ba1 Merge pull request #857 from obsproject/fix/remove-old-ifdefs
Base: Remove old ifdefs
2021-12-29 20:54:24 -08:00
195c4a3ca9 Merge pull request #873 from obsproject/fix/inputvolumemeters-check
ObsVolumeMeter: Reenable check for valid input
2021-12-29 20:54:00 -08:00
3b2369ae97 Requests: Add support check for SetInputAudioMonitorType 2021-12-29 20:50:27 -08:00
af634b63fd Merge pull request #854 from obsproject/request/removeinput
Requests: Enable RemoveInput
2021-12-29 20:49:09 -08:00
444685c89d Utils: Reenable check for valid input in volumemeter 2021-12-29 20:45:20 -08:00
05aba45809 Base: Remove old ifdefs
It was a very cool method to save our precious std::strtoll method,
but will no longer be needed on the next OBS release.
2021-12-29 20:40:49 -08:00
1f1a8926b1 Requests: Enable RemoveInput 2021-12-29 20:40:14 -08:00
0c5d4ba3fb Merge pull request #852 from obsproject/event/changing_events
Events: Reenable *Changing events (second time)
2021-12-29 20:39:33 -08:00
947450ce4e Revert "Revert "Events: Re-enable *Changing events""
This reverts commit c60d09246c.
2021-12-29 20:29:45 -08:00
63cc1f316d Merge pull request #850 from obsproject/request/getrecorddirectory
Requests: Enable GetRecordDirectory
2021-12-29 20:28:44 -08:00
38157579a6 Requests: Enable GetRecordDirectory 2021-12-29 19:53:10 -08:00
389cbd854c docs(ci): Update generated docs - 707ac3f [skip ci] 2021-12-29 08:54:22 +00:00
707ac3f7e3 docs: Increase complexity of G/SetProfileParameter 2021-12-29 00:51:50 -08:00
732d5af50c EventHandler: Add Ui category 2021-12-29 00:34:25 -08:00
0939273abf EventSubscription: Add Ui category 2021-12-29 00:32:28 -08:00
f8263caa03 RequestHandler: Add code comment to RemoveSceneItem 2021-12-29 00:23:30 -08:00
5c16a11ba8 docs(ci): Update generated docs - af217c0 [skip ci] 2021-12-29 07:54:48 +00:00
af217c05f1 EventHandler: [BREAKING] Rename CurrentSceneChanged to CurrentProgramSceneChanged
Now matches the requests
2021-12-28 23:53:58 -08:00
e640ae1218 docs: Finish documenting events 2021-12-28 23:45:11 -08:00
bb2c125601 Utils: Use atomic in VolumeMeter manager 2021-12-28 22:50:11 -08:00
e1cb858d2d Utils: Split monitor type util 2021-12-28 22:49:58 -08:00
1339202c02 docs: Fix Array fields in transition requests 2021-12-28 22:37:00 -08:00
4835abd9c2 docs(ci): Update generated docs - 964e91b [skip ci] 2021-12-29 03:04:15 +00:00
964e91bbd7 docs: Document ObsMediaInputAction 2021-12-28 18:57:02 -08:00
9385a2449e docs: Document the rest of the undocumented requests 2021-12-28 18:28:04 -08:00
ec79124b5f docs: Document transition requests 2021-12-28 18:11:30 -08:00
4d65c2adee Utils: Move GetListPropertyItems to utils 2021-12-28 17:16:41 -08:00
851a6f8c5a docs(ci): Update generated docs - e2f60b0 [skip ci] 2021-12-29 00:58:51 +00:00
e2f60b002e RequestHandler: Reorder Ui requests to new category 2021-12-28 16:58:28 -08:00
86506778ad RequestHandler: Add Ui category
Creates a new category specific to requests controlling the OBS UI.
2021-12-28 16:58:28 -08:00
0992f74fad RequestHandler: Use ValidateScene2 for GetSceneItemId 2021-12-28 16:58:28 -08:00
430e61bef7 RequestHandler: Use ValidateScene2 in ValidateSceneItem 2021-12-28 16:58:28 -08:00
da83de7503 Config: Fix firstload password generation 2021-12-25 00:57:09 -08:00
07249da400 RequestHandler: More transition requests 2021-12-23 21:00:11 -08:00
899888eb6c docs(ci): Update generated docs - 1423802 [skip ci] 2021-12-23 09:07:56 +00:00
14238027cc RequestHandler: More transition requests 2021-12-23 01:07:27 -08:00
5cbf439f55 Utils: Add GetSceneTransitionByName 2021-12-23 01:07:15 -08:00
e05be47847 RequestHandler: Add GetTransitionKindList 2021-12-22 16:28:12 -08:00
2302fdd25f Utils: Fix up transition related utils 2021-12-22 16:27:50 -08:00
1c6ec1dda2 RequestHandler: Add ResourceNotConfigurable 2021-12-22 16:08:39 -08:00
ad347c4823 RequestHandler: Add files for Transitions and Filters 2021-12-22 15:57:57 -08:00
714b4db840 deps: Update asio to 1.21.0 2021-12-22 15:23:42 -08:00
40ff3f6960 Base: More code/comment nitpicks 2021-12-21 20:35:08 -08:00
0f303504e1 Base: Nitpick cleanup for obs-websocket.cpp 2021-12-21 20:35:08 -08:00
91e3f5ee18 EventHandler: Remove platform include
Already included by obs-websocket.h, but does not have ifdefs
protecting strtoll. Better to just remove it.
2021-12-21 20:35:08 -08:00
66f416236c docs(ci): Update generated docs - b331f76 [skip ci] 2021-12-21 11:10:11 +00:00
b331f76d40 Utils: Use output path util instead of hacky method
We still have to wait for a new OBS version to be released before this
will work, but to be fair it was pretty broken in the previous state.
2021-12-21 03:09:45 -08:00
a898bacd79 Utils: Rename VolumeMeter utils for consistency 2021-12-21 03:09:45 -08:00
527a008002 RequestHandler: Remove OutputStartFailed request status
Accidentally pushed this commit when it was for a component which will
not be implemented.
2021-12-21 03:09:45 -08:00
a0ad43c7d7 docs(ci): Update generated docs - f566ccd [skip ci] 2021-12-21 09:32:37 +00:00
f566ccd76b Utils: Add output state util to utils 2021-12-21 01:32:12 -08:00
2e8622e8d7 RequestHandler: Fix IsValid() input parameter 2021-12-21 01:32:12 -08:00
4a193d44a1 RequestHandler: Add OutputStartFailed request status 2021-12-21 01:32:12 -08:00
d29b87ffc1 EventHandler: Remove unused file 2021-12-21 01:32:12 -08:00
1e6a60f545 Utils: Rename ListHelper to ArrayHelper 2021-12-21 01:32:12 -08:00
5cd1af426a Utils: Rename DataHelper to ObjectHelper 2021-12-21 01:32:12 -08:00
f66080a031 Utils: Split Obs utils into individual files 2021-12-21 01:32:12 -08:00
82ad3313e8 docs: More docs 2021-12-21 01:32:12 -08:00
71bf9e9021 docs(ci): Update generated docs - 0c7fda2 [skip ci] 2021-12-19 10:11:36 +00:00
0c7fda28a0 Merge pull request #881 from dnaka91/fix-wrong-opcode-formatting
protocol: Fix error formatting for wrong op codes
2021-12-19 17:11:30 +07:00
e2804e2d85 Merge pull request #880 from dnaka91/input-settings-overlay
docs: Document missing `overlay` field
2021-12-19 17:11:18 +07:00
b37305354d Merge pull request #882 from dnaka91/ci-macos-10.13
ci: Set the minimum MacOS version to 10.13
2021-12-19 17:10:56 +07:00
6f6fbf84d1 ci: Set the minimum MacOS version to 10.13 2021-12-19 17:35:34 +09:00
9749502e88 protocol: Fix error formatting for wrong op codes 2021-12-19 17:30:58 +09:00
c3e6bc323a Adjust spacing of docs 2021-12-19 17:23:22 +09:00
873ad1b167 docs: Document missing overlay field 2021-12-19 16:31:31 +09:00
a40e79e987 Utils/Obs: Fix build (again) 2021-12-17 15:48:59 -08:00
84e649a6f7 Utils: Tweak some includes 2021-12-17 14:47:56 -08:00
c4ab69481b Base: Move AutoRelease helpers to utils + build fix
- Moves the AutoRelease helpers to utils/Obs.h
- Fixes build using obsproject/obs-studio/pull/5580
2021-12-17 14:35:19 -08:00
edf4e942fa RequestHandler: Use correct output in GetRecordStatus
It was using the stream output due to a glitch, surprised nobody
reported it yet.
2021-12-17 02:38:56 -08:00
b3ef9a861e docs(ci): Update generated docs - 54fd7af [skip ci] 2021-12-17 07:53:30 +00:00
54fd7af5ef Merge pull request #878 from dnaka91/fix-scenes-type
docs: Fix `scenes` type from `String` to `Object`
2021-12-17 14:53:12 +07:00
2a33179588 docs: Fix scenes type from String to Object 2021-12-17 16:37:27 +09:00
906c6c4871 docs(ci): Update generated docs - f7ab102 [skip ci] 2021-12-17 06:57:34 +00:00
f7ab102c21 Merge pull request #877 from dnaka91/supported-image-formats
docs: Add missing `supportedImageFormats` to docs
2021-12-17 13:57:14 +07:00
fdc8b5546f docs(ci): Update generated docs - 6f72da8 [skip ci] 2021-12-17 06:57:01 +00:00
6f72da83ff Merge pull request #874 from dnaka91/fix-markdown-arrays
docs: Fix `Array` type in markdown
2021-12-17 13:56:41 +07:00
0007987219 docs(ci): Update generated docs - 77f8c5b [skip ci] 2021-12-17 06:56:16 +00:00
77f8c5be4d Merge pull request #876 from dnaka91/scenes-wrong-category
docs: Correct the category for scene requests
2021-12-17 13:55:53 +07:00
64c3b62360 docs: Add missing supportedImageFormats to docs 2021-12-17 15:16:37 +09:00
c6afc8f981 docs: Fix Array type in markdown 2021-12-17 15:08:33 +09:00
2c2b584ecc docs: Correct the category for scene requests 2021-12-17 15:00:47 +09:00
edda844a34 docs(ci): Update generated docs - d642654 [skip ci] 2021-12-17 04:01:59 +00:00
d642654f49 Merge pull request #875 from dnaka91/slotValue-response-type
docs: Adjust `slotValue` type to `Any`
2021-12-17 11:01:40 +07:00
69522024d2 docs: Adjust slotValue type to Any 2021-12-17 12:21:39 +09:00
24cd95bca7 docs(ci): Update generated docs - 0f6ee87 [skip ci] 2021-12-15 10:42:02 +00:00
0f6ee87f99 WebSocketServer: [BREAKING] Remove ignoreInvalidMessages identify param
This parameter is a weird one. With the abstraction of requests from
the underlying websocket protocol, there theoretically should be no
need to ignore invalid messages, because the implementation of the
low-level protocol on clients should be solid, with the requests
themselves not being fatal to the session.

As such, I consider this to be feature bloat, with lots of messy code
attributed to it.
2021-12-15 02:41:37 -08:00
9c8f056d3e docs(ci): Update generated docs - 41a145c [skip ci] 2021-12-15 10:13:28 +00:00
41a145c57c SettingsDialog: Only restart when necessary
The previous few commits now allow for us to change settings without
restarting the WebSocket server.
2021-12-15 02:11:33 -08:00
fab56d71ea WebSocketServer: Get config values directly from Config
We were previously storing config values in the WebSocketServer class
itself, in a weak attempt at modularity. It's better to give up on that
idea and just get it from the Config object itself.
2021-12-15 02:06:52 -08:00
b490e4409b CMakeLists: Add runtime tests flag 2021-12-15 02:06:31 -08:00
8fbcbad9ec Config: Make values atomic 2021-12-15 02:06:16 -08:00
82d5468b73 EventHandler: [BREAKING] Rename ExternalPluginEvents to Vendors
Make it fit the rest of the naming
2021-12-14 21:51:30 -08:00
2e7262fe11 docs: Document plugin api event 2021-12-14 18:33:50 -08:00
5f261de143 WebSocketApi: Finish implementations 2021-12-14 18:22:52 -08:00
539ee3f28f obs-websocket-api: Various improvements
Make functions static and code nitpicks
2021-12-14 18:22:21 -08:00
29a5cfe2fe Utils: Include algorithm
Good thing to have
2021-12-14 18:02:13 -08:00
612a50efbb docs(ci): Update generated docs - 2042692 [skip ci] 2021-12-15 01:42:11 +00:00
20426924cd Utils: [BREAKING CHANGE] Remove groups from GetSceneList + re-add order
- Removes the `isGroup` boolean field from the scene object, and does
not include any groups in the returned array.
- Reintroduces ordered results. Previous versions used a method which
did not return the scene list in the same order as the UI. This change
also means that this request is more susceptible to crashing OBS if
called during a scene collection change.
- Adds the `sceneIndex` number to the scene object. 0 being the bottom
of the scene list, just like in other requests like `GetSceneItemList`.
2021-12-14 17:37:06 -08:00
889062e44b RequestHandler: Reorder BroadcastCustomEvent
Code cleanup stuff
2021-12-13 19:20:27 -08:00
eb8d69dca5 Base: Move request batch processing to requesthandler directory
Request batch processing had less to do with the protocol/server and
more to do with the actual request handler, so it felt better to move
it.
2021-12-13 19:10:08 -08:00
0ed3c9b367 docs(ci): Update generated docs - e47de63 [skip ci] 2021-12-13 20:33:51 +00:00
e47de63786 docs: Fix request type typo 2021-12-13 12:33:35 -08:00
84f90a9650 docs(ci): Update generated docs - 3ea8506 [skip ci] 2021-12-13 00:54:23 +00:00
3ea8506619 Merge pull request #869 from BarRaider/doc-fix-opcode
docs: fixed wrong opcode in docs
2021-12-12 16:54:01 -08:00
d4353d4bf1 docs: fixed wrong opcode in docs 2021-12-12 23:42:13 +02:00
d7887b4c32 WebSocketServer: Fix execution type 2021-12-11 23:28:37 -08:00
de1f843ce6 WebSocketServer: Swap variable behavior and fix crash caused by const parameters 2021-12-10 23:58:14 -08:00
5ac813b897 WebSocketServer: Tiny code cleanup 2021-12-10 22:33:52 -08:00
43e2860709 WebSocketServer: Implement haltOnFailure for batch requests 2021-12-10 22:28:22 -08:00
c9c5da6837 RequestBatchExecutionType: Change executionType values
Fits better for the front-facing API.
2021-12-10 22:00:02 -08:00
b66d2284b3 WebSocketOpCode: Add IsValid check
Not used, but useful addition.
2021-12-10 21:59:13 -08:00
fcbe11616d docs: Overhaul documentation (#863)
More docs-related commits will follow, but this needs to be merged in order to continue with other development.

* Docs: Overhaul docs generator (beginning)

* docs: Rename comments file

* docs: Move comments gitignore

* docs: Initial request documentation

* docs: Improvements to comment processing

* docs: More improvements

* docs: Add enum functionality for protocol.json

* WebSocketServer: Document enums

* RequestHandler: Document RequestStatus enum

* Base: Move ObsWebSocketRequestBatchExecutionType to its own file

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

* docs: Update introduction header

Removes the enum section, and documents RequestBatchExecutionType.

* WebSocketCloseCode: Shuffle a bit

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

* RequestStatus: Mild shuffle

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

* docs: Delete old files

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

* docs: Add enum identifier value

Forgot to add this before, oops

* docs: Document more enums

* docs: Add basic protocol.md generator

* docs: More work on MD generator

* docs: MD generator should be finished now

* docs: More fixes

* docs: More fixes

* docs: More tweaks + add readme

* docs: Update readme and add inputs docs

* docs: More documentation
2021-12-10 21:38:18 -08:00
6cec018c8d WebSocketServer: Fix null check for request batch variables 2021-12-10 21:20:48 -08:00
6d684eb07f ObsVolumeMeter: Don't use obs_weak_source_expired()
This function is not released yet.

TODO: Reenable its usage when OBS 27.2.0 is released.
2021-11-22 20:14:54 -08:00
5704ea2970 Requests: Increase InputVolumeMeters update rate to 20/sec
We were using a 60ms interval before, which just feels too slow.
A 50ms interval feels much better.
2021-11-22 03:51:41 -08:00
d48ddef031 EventHandler: Implement InputVolumeMeters
This is probably one of the most requested features for obs-websocket.
This currently works by firing an event to all explicit subscribers
with an array of all active audio sources every **60 milliseconds.**

The `inputLevelsMul` field follows this data format:

Base: [Channel, Channel]
Channel: [magnitude (mul), peak (mul), input_peak (mul)]

           *Not Muted*         *Muted*
Example: [[0.3, 0.5, 0.9], [0.0, 0.0, 0.0]]

(input_peak is the actual peak value, before volume adjustment.)

You may notice that the values are only in mul. This is because we are
trying to cut down on bandwidth. dB values can be calculated using this
formula:

`dB = 20.0 * log10(mul)`
2021-11-22 03:37:56 -08:00
1ac6ac6c87 plugin-macros: Remove extra [obs-websocket] tag 2021-11-21 17:51:53 -08:00
bc7b8d330a RequestHandler: Move RequestStatus.h to types directory 2021-11-21 03:13:53 -08:00
c95511eb5f Base: Remove UNUSED_PARAMETER() usages
I prefer to use undefined parameters instead.
2021-11-21 03:08:06 -08:00
2a4e86d8da Base: Add more module exports + code cleanup 2021-11-21 02:50:41 -08:00
4d8013b07e SettingsDialog: Add 6 character minimum length
Security requirements should be held just like every other platform
out there, even if they are lax
2021-11-21 02:29:21 -08:00
b1de9c8e79 Base: General cleanup
- Moves unnecessary .md files to the wiki tab
- Updates some links I forgot to update before
- Updates the editorconfig
- Cleans up the README
2021-11-21 02:10:41 -08:00
8e9b3ef7bb ISSUE_TEMPLATE: Move to new standard
In order to overwrite what the org uses, we must update our issue
template to the new system.
2021-11-21 01:38:20 -08:00
2e079ad681 WebSocketServer: Various code cleanup 2021-11-21 01:37:21 -08:00
29a72f9af8 WebSocketServer: Use WebSocketOpCode where possible 2021-11-21 01:37:21 -08:00
0a294a558e WebSocketServer: Move enums to types directory
Code cleanup, makes WebSocketServer match how enums are handled in
other parts of the plugin
2021-11-21 01:37:21 -08:00
959347337f lib: Add plugin example + slightly change names 2021-11-21 01:37:21 -08:00
c8c6417d63 FUNDING: Update links
Our primary funding source is now Open Collective
2021-11-21 01:08:34 -08:00
2005ced682 README: Remove dev header 2021-11-20 13:42:55 -08:00
4ca259b790 Base: Enforce -Wall and fix compiler warnings
That was *a lot* of warnings.
2021-11-20 02:34:48 -08:00
c720df5938 lib: Fix build on <c++20
C99 and C++20 are the minimum versions that support dedicated
initializer lists.
2021-11-20 01:58:16 -08:00
32a9e12f62 Base: Logging improvements
Use blog_debug() in place of almost all debug logging messages, and
change some log levels to be less verbose.
2021-11-20 01:50:49 -08:00
60f12a16f3 plugin-macros: Add blog_debug()
obs-studio's LOG_DEBUG setting only works in very specific
circumstances, which is why we implement our own debug logging. This
will help a lot of code cleanup.
2021-11-20 01:46:01 -08:00
99cbaaf34c Base: Implement ObsWebSocketApi + cleanup
- Implements a WIP ObsWebSocketApi, for obs-websocket-api.h. Events are
finished, but requests are not.
- Some logging improvements
- A bit of code cleanup around the plugin
2021-11-20 01:26:50 -08:00
bc1d5386a5 Base: Add external plugin library
This header file is meant to be included by 3rd party OBS plugins,
which allows them to add custom requests and events to obs-websocket.

*not finished yet*
2021-11-19 18:10:01 -08:00
bd6c663775 Base: Add copyright header to source 2021-11-19 17:32:22 -08:00
18ed1589ae Base: Update obs-websocket git locations
obs-websocket has moved to the obsproject
2021-11-19 17:24:15 -08:00
07a20b6458 Merge pull request #856 from dnaka91/record-pause-resume
Events: Add events for record pause and resume
2021-11-18 22:28:04 -08:00
6cb8eef96d Events: Add events for record pause and resume 2021-11-19 15:24:00 +09:00
5ab091a40b Base: Move WebSocketServer to its own directory
Just helps with organization
2021-11-18 00:29:28 -08:00
c60d09246c Revert "Events: Re-enable *Changing events"
This reverts commit 32be21886c.
2021-11-17 03:16:42 -08:00
061c228ad5 Merge pull request #851 from obs-websocket/event/changing_events
Put normal commits on this branch by accident dammit
2021-11-17 03:16:15 -08:00
4076c0baa9 Requests: Add media input requests 2021-11-17 03:03:14 -08:00
01013c1b27 Base: Fix compile error
I literally could have sworn I remember json::null() being valid, but
I guess not. Oops
2021-11-17 02:58:11 -08:00
1dbb7a9686 Requests: Apply some code style changes 2021-11-17 02:14:13 -08:00
061fb6f012 Base: Update various links
Now that obs-websocket has been moved to an org, let's update
any links accordingly.
2021-11-17 01:37:37 -08:00
32e4ad74b7 CONTRIBUTING: Update standards
Update some links and standards with what we've been using
2021-11-17 01:32:37 -08:00
0f17d3d6f8 Utils/Obs: Update StringHelper function names
I hated having `String` appended to half of the utils since it was
extra
2021-11-17 01:23:23 -08:00
08eb2defbc Requests: Add GetMediaInputState 2021-11-17 01:17:40 -08:00
7403264d42 Requests/Utils: Modify utils naming/usage
A bit too niche to have a util that takes an output and returns a
timecode string. Let's just make it take a duration in milliseconds.
2021-11-17 01:07:04 -08:00
32be21886c Events: Re-enable *Changing events 2021-11-17 00:37:50 -08:00
d5a702b0e8 Events: Disable *Changing events
CI fails right now because they are not yet included in an OBS release.
2021-11-17 00:36:53 -08:00
0671ded7d1 Requests: Add recording requests 2021-11-17 00:30:17 -08:00
9197a48088 Requests: Add SetSceneItemTransform
Yay party
2021-11-12 23:24:39 -08:00
5faadc12de Utils/Obs: Add scaleX and scaleY plus new util 2021-11-12 23:23:51 -08:00
45f7661a5d Requests: Add DuplicateSceneItem 2021-11-12 17:31:11 -08:00
aa46bb74a5 RequestHandler/RPC: Add ValidateScene2 2021-11-12 17:30:46 -08:00
af52a26e68 Utils: Add ability to specify transform/crop for scene item creation 2021-11-12 17:29:43 -08:00
69494d9c85 Base: Add OBSSceneAutoRelease 2021-11-12 17:19:57 -08:00
152faa5fe2 Events: Add profile/scene collection change events 2021-11-12 15:20:01 -08:00
622e1c9aeb Merge pull request #842 from dnaka91/macos-scripts
Build: Make scripts executable and fix dep links
2021-10-29 17:20:18 -07:00
ee3216968d Build: Make scripts executable and fix dep links 2021-10-24 14:54:34 +09:00
4e956d1ef4 Requests: Add note SetCurrentSceneCollection 2021-10-06 00:45:14 -07:00
42c78f1831 Events: Prepare to add a few new OBS events
Pending PR merge and version release
2021-10-06 00:44:42 -07:00
1b25e98dc2 Revert "Requests: Add Compare"
This reverts commit 3687086ce0.

I realized that I'm adding too much too fast, and that we should
focus on getting v5 out first.
2021-10-05 20:17:45 -07:00
3687086ce0 Requests: Add Compare
Non-functional, will have to finish impl later
2021-10-04 17:32:17 -07:00
f0b207d021 Base: Add variable support to request batches + refactor
- Adds variables to execution types SERIAL_REALTIME and SERIAL_FRAME
- Pass by reference where copy is unnecessary
- Start WebSocket server after OBS finishes loading instead of on
plugin load
2021-10-01 17:34:09 -07:00
16ea2c82e1 Requests: Verify current program scene
Crashes can occur if there is no current program scene. Currently
caused by connecting to obs-websocket and calling `GetSceneList`
before OBS has actually finished loading.
2021-10-01 17:20:38 -07:00
981538aa2a Base: Make some items atomic 2021-09-30 19:14:17 -07:00
9f71e4af2c Merge pull request #829 from dnaka91/add-obws-reference
README: Add Rust obws to library list
2021-09-28 11:52:34 -07:00
0fc813f48b README: Add Rust obws to library list 2021-09-28 23:34:30 +09:00
df7af451a7 Requests: Fix some parameter validation
Thanks to t2t2 for the report over Discord
2021-09-26 03:54:16 -07:00
a8d27ede9e Base: Add request batch execution types
A new `executionType` field has been added to the `RequestBatch` Op

Types added:
- `OBS_WEBSOCKET_REQUEST_BATCH_EXECUTION_TYPE_SERIAL_REALTIME`(default)
- `OBS_WEBSOCKET_REQUEST_BATCH_EXECUTION_TYPE_SERIAL_FRAME`
- `OBS_WEBSOCKET_REQUEST_BATCH_EXECUTION_TYPE_PARALLEL`

`OBS_WEBSOCKET_REQUEST_BATCH_EXECUTION_TYPE_SERIAL_REALTIME`:
- Same as how request batches have always worked.
- Requests are processed in-order
- Requests are processed as soon as possible by one worker thread
- The `Sleep` request blocks execution for a specified amount of real
world time

`OBS_WEBSOCKET_REQUEST_BATCH_EXECUTION_TYPE_SERIAL_FRAME`:
- New!
- Requests are processed in-order
- Requests are processed on the graphics thread. BE VERY CAREFUL NOT
TO OVERLOAD THE GRAPHICS THREAD WITH LARGE REQUESTS. A general rule
of thumb is for your request batches to take a maximum of 2ms per
frame of processing.
- Requests processing starts right before the next frame is composited.
This functionality is perfect for things like `SetSceneItemTransform`
- The `Sleep` request will halt processing of the request batch for
a specified number of frames (ticks)
- To be clear: If you do not have any sleep requests, all requests in
the batch will be processed in the span of a single frame
- For developers: The execution of requests gets profiled by the OBS
profiler under the `obs-websocket-request-batch-frame-tick` name.
This value (shown in the OBS log after OBS shutdown) represents the
amount of time that the graphics thread spent actively processing
requests per frame. This tool can be used to determine the amount of
load that your request batches are placing on the graphics thread.

`OBS_WEBSOCKET_REQUEST_BATCH_EXECUTION_TYPE_PARALLEL`:
- New!
- Requests are processed asynchronously at the soonest possible time.
- Requests are processed by the core obs-websocket thread pool, where
the number of workers == the number of threads on your machine.
  - If you have 12 threads on your machine, obs-websocket will be able
to process 12 requests at any given moment.
- The `results` array is populated by order of request completion.
Consider the order to be random.
- The `Sleep` request will return an error if attempted to be used in
this mode.
- Note: This feature is experimental and can increase the chances of
causing race conditions (crashes). While the implementation is fully
thread-safe, OBS itself is not. Usage of this is only recommended if
you are processing very large batches and need the performance benefit.
- Example use case: Performing `SaveSourceScreenshot` on 8 sources
at once.
2021-09-26 03:12:29 -07:00
45854e2949 Config: Properly add more logging, fix bug 2021-09-26 03:10:43 -07:00
181003af89 Utils: Use emplace_back instead of push_back
Saves copy operations
2021-09-25 19:34:05 -07:00
2218b7956c Requests: Support groups in most of the scene item requests
Tested each request to the fullest reasonable extent. Requests performed properly without errors or leaks
2021-09-25 18:59:04 -07:00
0e8650dbd2 Requests: Support groups in GetSceneItemId 2021-09-25 18:45:37 -07:00
da4297ee88 Request: Clarify error messages 2021-09-25 18:45:00 -07:00
5350c115bf Requests: Improve more request checks 2021-09-25 18:29:27 -07:00
d09571f0e3 Requests: Improve some check logic in GetSourceActive 2021-09-25 18:20:01 -07:00
d04d42240a Request: Fix scene filter functionality
Previous functionality did not support groups.
2021-09-25 18:14:25 -07:00
9ca83e7570 Requests: Make GetSceneItemId scene only 2021-09-25 17:54:39 -07:00
aa0ba78e46 Base: Use more refcounts
Use refcounts more effectively
2021-09-25 17:52:08 -07:00
b91ad0f790 Requests: Add GetSceneItemId
I realized that it was not entirely fair to expect users to fetch a
scene's item list, then search the list on the client in order to find
an item ID, so this is a compromise. This will also help developers
move from the 4.x scene item functionality to 5.x's

Operating on scene items by relying on source name can be dangerous
and in some cases exhibit what would be considered undefinied behavior.
Operating on scene items using IDs is best practice.
2021-09-25 17:21:06 -07:00
4e0b8c5f11 Utils/Obs: Add GetSceneItemByName
Simple, didn't really need to be a util tbh. But I added it anyway
and there's nothing you can do to stop me
2021-09-25 17:21:00 -07:00
ea948766a5 Events: Fix conversion warning
Thanks micolous for the report.
2021-09-24 18:54:48 -07:00
c73a153c9a Utils/Crypto: Use better method for GenerateSecret
Thanks to micolous for the suggestion
2021-09-24 18:39:46 -07:00
61973e75dc Revert "CI: Build against OBSProject Qt distribution"
This reverts commit 59e1083557.
2021-09-24 18:26:34 -07:00
59e1083557 CI: Build against OBSProject Qt distribution 2021-09-24 18:14:26 -07:00
0c2e40263a WebSocketServer: Remove QtConcurrent dependency
We can avoid requiring QtConcurrent by using QRunnables. Thanks to
micolous for the idea.
2021-09-24 18:11:31 -07:00
78d02696d0 Utils: Add retrocompatability header
QRunnable::create was added in Qt 5.15, but Ubuntu still uses 5.12.
This reimplements that functionality until Ubuntu moves to >= 5.15
2021-09-24 18:10:17 -07:00
7a888c2f92 Config: Add more logging
Good to have
2021-09-24 18:09:46 -07:00
342164dfb5 Forms: Use QHideEvent instead of QCloseEvent
QCloseEvent is the wrong event to use here. If the `Ok` button is
pressed for example, QCloseEvent is not emitted. QHideEvent is
always called when the dialog is hidden.
2021-09-17 03:00:33 -07:00
a4e62acf25 Forms: Update ConnectInfo on settings apply instead of close
Just for better usability
2021-09-17 02:54:29 -07:00
d811c95e10 Requests: Add some more failure checks and improve response codes 2021-09-17 02:43:20 -07:00
eebcc25115 Requests: Add unpublished RemoveInput
The functionality depends on obs-studio#5276 to be merged and released,
so we can add it but not enable it for now.
2021-09-17 02:42:58 -07:00
c9fa09edc4 Config/Base: Refactor with fixes
Fixes these things:
- Websocket password is not generated if FirstLoad and overridden
- Save generated password immediately if FirstLoad
- Do not generate new password if FirstLoad and password already exists
- More logging
2021-09-17 02:25:30 -07:00
69ccc99921 Utils/UI: Make GeneratePassword() use std::string
Just for consistency
2021-09-17 02:09:44 -07:00
a5a19b9952 Utils/Crypto: Use QRandomGenerator instead of qrand()
qrand() is obsolete, and QRandomGenerator produces numbers seeded from
the platform's RNG source. Makes our authentication system way more
secure too.
2021-09-17 02:00:30 -07:00
66ff329da4 Config: Fix typo 2021-09-15 03:46:52 -07:00
af0ac63f2c Installer: Fail if OBS is not installed 2021-09-12 00:22:11 -07:00
802cb38ff6 README: Move simpleobsws to the correct section 2021-09-10 01:37:59 -07:00
117dcb9567 Merge pull request #815 from dnaka91/fix-validate-basic
Requests: Fix wrong validation in ValidateBasic
2021-09-06 06:43:38 -07:00
d20c0d0da7 Reqeusts: Fix wrong validation in ValidateBasic 2021-09-06 10:48:01 +09:00
a9d86ce35c docs(ci): Update generated docs - b9b8e38 [skip ci] 2021-09-04 17:58:05 +00:00
b9b8e38998 WebSocketServer: Remove ignoreNonFatalRequestChecks from session params 2021-09-04 10:57:00 -07:00
e89c0c2b05 Base: More code cleanup and fixes 2021-09-04 10:47:51 -07:00
7e1e1bc33c Base: Large plugin refactor
- Merge WebSocketProtocol into WebSocketServer
  - Having them separated was not doing anything productive
- Request: Move SessionPtr to RequestHandler
  - Less copying to do for batch requests
- Fully modularize EventHandler
  - Make BroadcastEvent a stored callback that WebSocketServer sets
- Return early on high volume events to avoid unnecessary compute
  - These events will only generate a json object when it is actually
needed
2021-09-04 10:04:00 -07:00
537595658d Requests: Add GetSceneItemTransform 2021-09-03 15:59:09 -07:00
c43d829845 Events: Complete SceneItemTransformChanged 2021-09-03 15:58:50 -07:00
3d2fb65357 Utils: Add GetSceneItemTransform 2021-09-03 15:58:34 -07:00
96dcc49adb Requests: More requests and code cleanup
(plus very slight performance improvement)
2021-09-03 12:52:50 -07:00
25b3bd44ba RequestHandler/RPC: Improve code efficiency and add optionals 2021-09-03 12:52:15 -07:00
bcdb8ee352 Events: Fix SceneItemListReindexed event name 2021-09-03 08:59:09 -07:00
333737f400 Requests: Additions and code cleanup 2021-09-02 19:29:13 -07:00
82d8a3d7ce Utils: Add stuff to Obs 2021-09-02 19:28:55 -07:00
d7f96b6dea Revert "Request: Minor code cleanup"
This reverts commit c16669c7b0.
2021-09-02 13:33:20 -07:00
c16669c7b0 Request: Minor code cleanup 2021-09-02 13:25:17 -07:00
0269209d59 Requests: Use OBS naming for monitorType enum 2021-09-02 11:24:48 -07:00
d2d2bdd730 docs(ci): Update generated docs - 9a8587d [skip ci] 2021-09-01 17:44:19 +00:00
9a8587d6df Requests: Check for last scene in RemoveScene 2021-09-01 10:43:36 -07:00
8a45560297 Utils: Add GetSceneCount() 2021-09-01 10:43:24 -07:00
fb0656c31e RequestHandler: Simplify request statuses 2021-09-01 10:30:40 -07:00
26bef074ac EventHandler: Add (disabled) InputAudioMonitorTypeChanged event 2021-08-31 06:39:09 -07:00
e18aaff661 RequestStatus: Add new statuses to replace old soon 2021-08-31 06:38:55 -07:00
4271730dc2 Requests: Add more requests 2021-08-30 13:55:22 -07:00
b86107a699 Base: Add OBSPropertiesAutoDestroy 2021-08-30 13:55:05 -07:00
6035f258d2 docs(ci): Update generated docs - a40160e [skip ci] 2021-08-30 17:01:00 +00:00
a40160e305 WebSocketServer: Use Sec-WebSocket-Protocol for json/msgpack 2021-08-30 09:59:59 -07:00
b58f6e8366 WebSocketServer: Remove unnecessary string usage 2021-08-30 04:46:59 -07:00
670fa7c249 CI: More fixes 2021-08-30 04:15:55 -07:00
d858118e28 CI: Fix oopsies 2021-08-30 03:42:32 -07:00
bb71a4c77b CI: Prefix checkinstall version to avoid errors 2021-08-30 03:29:30 -07:00
3dfd091e71 CI: Provide package version suffix to cmake 2021-08-30 03:17:09 -07:00
a3d0ff5eea CMakeLists: More changes for version suffix 2021-08-30 03:08:09 -07:00
5988f0f97a Installer: Use OBS_WEBSOCKET_VERSION instead of CMAKE_PROJECT_VERSION 2021-08-30 02:52:21 -07:00
eb6015df05 Base: Remove generated installer file 2021-08-30 02:51:35 -07:00
15188e3ebe GitIgnore: Ignore generated installer file 2021-08-30 02:51:14 -07:00
1ecf2a4fdb CMakeLists: Allow specifying version suffix string 2021-08-30 02:50:02 -07:00
292b2b0d3b Base: Remove generated plugin macros file 2021-08-30 02:49:43 -07:00
b3676586e4 GitIgnore: Ignore generated plugin macros 2021-08-30 02:49:07 -07:00
6d882ba94f Merge branch 'master' of https://github.com/Palakis/obs-websocket 2021-08-30 02:31:03 -07:00
d669db24ac CI: Update some stuff 2021-08-30 02:30:48 -07:00
fc8dce45ee docs(ci): Update generated docs - ac78acd [skip ci] 2021-08-30 08:47:29 +00:00
ac78acd28c CI: Try to fix tag recognition (again) 2021-08-30 01:46:52 -07:00
f37edbd71c EventHandler: Include required event intent in emit 2021-08-30 01:39:46 -07:00
78f9c93739 Requests: Fix global realm storage on persistent data req's 2021-08-30 01:32:22 -07:00
96c5818395 ConnectInfo: Update QR code format 2021-08-30 01:11:29 -07:00
85fa41962d Merge branch 'master' of https://github.com/Palakis/obs-websocket 2021-08-29 17:02:29 -07:00
ccb42f1f0c README: Add simpleobsws
It's finally in beta, so we can go ahead and add it as officially
supporting 5.0
2021-08-28 05:33:06 -07:00
137 changed files with 22540 additions and 3951 deletions

View File

@ -1,10 +1,17 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
[*.{c,cpp,h,hpp}]
indent_style = tab
indent_size = 4
[CMakeLists.txt]
indent_style = tab
indent_size = 4
[*.{yml,yaml}]
indent_style = space
indent_size = 2

4
.github/FUNDING.yml vendored
View File

@ -1,3 +1 @@
open_collective: obs-websocket
github: Palakis
custom: https://www.paypal.me/stephanelepin
open_collective: obs-websocket-dev

View File

@ -1,20 +0,0 @@
##### Issue type
<!--- Uncomment one of the two options below. -->
<!--- - Bug report -->
<!--- - Feature request -->
##### Description
<!--- Describe the bug encountered or feature requested. -->
##### Steps to reproduce and other useful info
<!--- If it's a bug, please describe the steps to reproduce it and PLEASE include an OBS log file. Otherwise, remove this section. -->
##### Technical information
- **Operating System:**
- **OBS Studio version:**
- **obs-websocket version:**
##### Development Environment
<!--- If you're trying to compile obs-websocket, please describe your compiler type and version (e.g: GCC 4.7, VC2013, ...), and the CMake settings used. -->
<!--- Remove this section if it does not apply. -->

130
.github/ISSUE_TEMPLATE/bug_report.yaml vendored Normal file
View File

@ -0,0 +1,130 @@
name: Bug Report
description: Report a bug or crash
title: "Bug: "
labels: ["Issue: Bug - Unconfirmed"]
body:
- type: markdown
id: md_welcome
attributes:
value: This form is for reporting bugs for obs-websocket!
- type: dropdown
id: os_info
attributes:
label: Operating System Info
description: What Operating System are you running?
options:
- Windows 11
- Windows 10
- Windows 8.1
- macOS 12.0
- macOS 11.6
- macOS 11.5
- macOS 11.4
- macOS 11.3
- macOS 11.2
- macOS 11.1
- macOS 11.0
- macOS 10.15
- macOS 10.14
- macOS 10.13
- Ubuntu 21.04
- Ubuntu 20.10
- Ubuntu 20.04
- Ubuntu 18.04
- Other
validations:
required: true
- type: input
id: os_info_other
attributes:
label: Other OS
description: "If \"Other\" was selected above, what OS are you using?"
placeholder: "e.g., Arch Linux, FreeBSD"
validations:
required: false
- type: dropdown
id: obs_version
attributes:
label: OBS Studio Version
description: What version of OBS Studio are you using?
options:
- 27.1.3
- 27.1.1
- 27.1.0
- 27.0.1
- Git
- Other
validations:
required: true
- type: input
id: obs_version_other
attributes:
label: OBS Studio Version (Other)
description: "If \"Other\" was selected above, what version of OBS Studio are you using?"
validations:
required: false
- type: dropdown
id: obs_websocket_version
attributes:
label: obs-websocket Version
description: What version of obs-websocket are you using?
options:
- 5.0.0-beta1
- 5.0.0-alpha3
- 5.0.0-alpha2
- 4.9.1
- 4.9.0
- Git
validations:
required: true
- type: input
id: obs_log_url
attributes:
label: OBS Studio Log URL
description: Please provide the obsproject.com URL (from Help menu > Log Files > Upload Current/Last Log File) to the OBS log file where this issue occurred.
validations:
required: true
- type: input
id: obs_crash_log_url
attributes:
label: OBS Studio Crash Log URL
description: If this is a crash report, please provide the obsproject.com URL to the OBS crash log file where this issue occurred.
validations:
required: false
- type: textarea
id: expected_behavior
attributes:
label: Expected Behavior
description: "What did you expect to happen?"
validations:
required: true
- type: textarea
id: current_behavior
attributes:
label: Current Behavior
description: "What actually happened?"
validations:
required: true
- type: textarea
id: steps_to_reproduce
attributes:
label: Steps to Reproduce
description: "How do you trigger this bug? Please walk us through it step by step."
placeholder: |
1.
2.
3.
...
value: |
1.
2.
3.
...
validations:
required: true
- type: textarea
id: additional_notes
attributes:
label: Anything else we should know?
validations:
required: false

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Help/Support
url: https://discord.gg/UjfPmYdRPZ
about: Development-related help for obs-websocket

View File

@ -0,0 +1,42 @@
name: Feature Request
description: Request for a new feature (request/event) to be added to obs-websocket
title: "Feature Request: "
labels: ["Issue: Feature Request"]
body:
- type: markdown
id: md_welcome
attributes:
value: This form is for requesting features for obs-websocket!
- type: dropdown
id: feature_request_type
attributes:
label: Feature Request Type
description: What kind of feature would you like to see added to obs-websocket?
options:
- RPC Request
- RPC Event
- Settings Dialog
- Other
validations:
required: true
- type: input
id: feature_request_type_other
attributes:
label: Feature Request Type (Other)
description: "If \"Other\" was selected above, what type of feature request do you have?"
validations:
required: false
- type: textarea
id: requested_feature
attributes:
label: Requested Feature
description: "What feature would you like to see added?"
validations:
required: true
- type: textarea
id: requested_feature_scenario
attributes:
label: Requested Feature Usage Scenario
description: "What is a use-case where this feature would be helpful?"
validations:
required: true

View File

@ -1,6 +1,6 @@
<!--- Please fill out the following template, which will help other contributors review your Pull Request. -->
<!--- Make sure youve read the contribution guidelines here: https://github.com/Palakis/obs-websocket/blob/master/CONTRIBUTING.md -->
<!--- Make sure youve read the contribution guidelines here: https://github.com/obsproject/obs-websocket/wiki/Contributing-Guidelines -->
### Description
<!--- Describe your changes. -->
@ -18,18 +18,17 @@ Tested OS(s):
<!--- What types of changes does your PR introduce? Uncomment all that apply -->
<!--- - Bug fix (non-breaking change which fixes an issue) -->
<!--- - New request/event (non-breaking) -->
<!--- - Documentation change (a change to documentation pages) -->
<!--- - Enhancement (modification to a current event/request which adds functionality) -->
<!--- - Performance enhancement (non-breaking change which improves efficiency) -->
<!--- - Code cleanup (non-breaking change which makes code smaller or more readable) -->
<!--- - New request/event (non-breaking) -->
<!--- - Documentation change (a change to documentation pages) -->
<!--- - Other Enhancement (anything not applicable to what is listed) -->
### Checklist:
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [ ] I have read the [**contributing** document](https://github.com/Palakis/obs-websocket/blob/4.x-current/CONTRIBUTING.md).
- [ ] My code is not on the master branch.
- [ ] The code has been tested.
- [ ] I have read the [Contributing Guidelines](https://github.com/obsproject/obs-websocket/wiki/Contributing-Guidelines).
- [ ] All commit messages are properly formatted and commits squashed where appropriate.
- [ ] My code is not on the `master` branch.
- [ ] The code has been tested.
- [ ] I have included updates to all appropriate documentation.

View File

@ -1,356 +1,560 @@
name: "CI Multiplatform Build"
on:
push:
paths-ignore:
- 'docs/**'
branches:
- master
tags:
- '[45].[0-9]+.[0-9]+*'
pull_request:
paths-ignore:
- 'docs/**'
- '**.md'
branches:
- master
jobs:
windows:
name: 'Windows 32/64-bit'
runs-on: [windows-latest]
if: contains(github.event.head_commit.message, '[skip ci]') != true
env:
QT_CACHE_VERSION: '2' # Change whenever updating OBS dependencies URL, in order to force a cache reset
QT_VERSION: '5.15.2'
WINDOWS_DEPS_CACHE_VERSION: '1' # Change whenever updating Qt dependency URL, in order to force a cache reset
WINDOWS_DEPS_VERSION: '2019'
CMAKE_GENERATOR: "Visual Studio 16 2019"
CMAKE_SYSTEM_VERSION: "10.0"
steps:
- name: 'Add msbuild to PATH'
uses: microsoft/setup-msbuild@v1.0.2
- name: 'Checkout obs-websocket'
uses: actions/checkout@v2
with:
path: ${{ github.workspace }}/obs-websocket
submodules: 'recursive'
- name: 'Checkout OBS-Studio'
uses: actions/checkout@v2
with:
repository: obsproject/obs-studio
path: ${{ github.workspace }}/obs-studio
submodules: 'recursive'
- name: 'Get OBS-Studio Git Info'
shell: bash
working-directory: ${{ github.workspace }}/obs-studio
run: |
git fetch --prune --unshallow
echo "OBS_GIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
echo "OBS_GIT_TAG=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
- name: 'Checkout last OBS-Studio release (${{ env.OBS_GIT_TAG }})'
shell: bash
working-directory: ${{ github.workspace }}/obs-studio
run: |
git checkout ${{ env.OBS_GIT_TAG }}
git submodule update
- name: 'Get obs-websocket Git Info'
shell: bash
working-directory: ${{ github.workspace }}/obs-websocket
run: |
git fetch --prune --unshallow
GIT_HASH=$(git rev-parse --short HEAD)
echo "GIT_HASH=$GIT_HASH" >> $GITHUB_ENV
GIT_TAG=$(git describe --tags --abbrev=0)
echo "GIT_TAG=$GIT_TAG" >> $GITHUB_ENV
if [ "$GIT_TAG" ] ; then \
VERSION="$GIT_TAG" ; \
else \
VERSION="$GIT_HASH-git" ; \
fi
echo "PACKAGE_VERSION=$VERSION" >> $GITHUB_ENV
- name: 'Restore Cached Qt'
id: qtcache
uses: actions/cache@v2
with:
path: Qt_${{ env.QT_VERSION }}.7z
key: 'qtdep-${{ env.QT_CACHE_VERSION }} | ${{ runner.os }}'
restore-keys: |
qtdep-${{ env.QT_CACHE_VERSION }} | ${{ runner.os }}
- name: 'Download Prerequisite: Qt'
if: steps.qtcache.outputs.cache-hit != 'true'
run: |
curl -kLO https://tt2468.net/dl/Qt_${{ env.QT_VERSION }}.7z -f --retry 5 -C -
- name: 'Extract Prerequisite: Qt'
run: |
7z x Qt_${{ env.QT_VERSION }}.7z -o"${{ github.workspace }}\cmbuild\QT"
- name: 'Restore Cached OBS-Studio Dependencies'
id: obscache
uses: actions/cache@v2
with:
path: ${{ github.workspace }}\cmbuild\deps\**
key: 'obsdep-${{ env.WINDOWS_DEPS_CACHE_VERSION }} | ${{ runner.os }}'
restore-keys: |
obsdep-${{ env.WINDOWS_DEPS_CACHE_VERSION }} | ${{ runner.os }}
- name: 'Install Prerequisite: Pre-built OBS-Studio dependencies'
if: steps.obscache.outputs.cache-hit != 'true'
run: |
curl -kLO https://cdn-fastly.obsproject.com/downloads/dependencies${{ env.WINDOWS_DEPS_VERSION }}.zip -f --retry 5 -C -
7z x dependencies${{ env.WINDOWS_DEPS_VERSION }}.zip -o"${{ github.workspace }}\cmbuild\deps"
- name: 'Restore OBS-Studio 32-bit Build v${{ env.OBS_GIT_TAG }} from Cache'
id: build-cache-obs-32
uses: actions/cache@v2
env:
CACHE_NAME: 'build-cache-obs-32'
with:
path: ${{ github.workspace }}/obs-studio/build32
key: ${{ runner.os }}-${{ env.CACHE_NAME }}-${{ env.OBS_GIT_TAG }}
restore-keys: |
${{ runner.os }}-${{ env.CACHE_NAME }}-
- name: 'Configure OBS-Studio 32-bit'
if: steps.build-cache-obs-32.outputs.cache-hit != 'true'
working-directory: ${{ github.workspace }}/obs-studio
run: |
if(!(Test-Path -Path ".\build32")){New-Item -ItemType directory -Path .\build32}
cd .\build32
cmake -G "${{ env.CMAKE_GENERATOR }}" -A Win32 -DCMAKE_SYSTEM_VERSION="${{ env.CMAKE_SYSTEM_VERSION }}" -DQTDIR="${{ github.workspace }}\cmbuild\QT\${{ env.QT_VERSION }}\msvc2019" -DDepsPath="${{ github.workspace }}\cmbuild\deps\win32" -DCOPIED_DEPENDENCIES=NO -DCOPY_DEPENDENCIES=YES -DBUILD_BROWSER=OFF ..
- name: 'Build OBS-Studio 32-bit'
if: steps.build-cache-obs-32.outputs.cache-hit != 'true'
working-directory: ${{ github.workspace }}/obs-studio
run: |
msbuild /m /p:Configuration=RelWithDebInfo .\build32\libobs\libobs.vcxproj
msbuild /m /p:Configuration=RelWithDebInfo .\build32\UI\obs-frontend-api\obs-frontend-api.vcxproj
- name: 'Restore OBS-Studio 64-bit Build v${{ env.OBS_GIT_TAG }} from Cache'
id: build-cache-obs-64
uses: actions/cache@v1
env:
CACHE_NAME: 'build-cache-obs-64'
with:
path: ${{ github.workspace }}/obs-studio/build64
key: ${{ runner.os }}-${{ env.CACHE_NAME }}-${{ env.OBS_GIT_TAG }}
restore-keys: |
${{ runner.os }}-${{ env.CACHE_NAME }}-
- name: 'Configure OBS-Studio 64-bit'
if: steps.build-cache-obs-64.outputs.cache-hit != 'true'
working-directory: ${{ github.workspace }}/obs-studio
run: |
if(!(Test-Path -Path ".\build64")){New-Item -ItemType directory -Path .\build64}
cd .\build64
cmake -G "${{ env.CMAKE_GENERATOR }}" -A x64 -DCMAKE_SYSTEM_VERSION="${{ env.CMAKE_SYSTEM_VERSION }}" -DQTDIR="${{ github.workspace }}\cmbuild\QT\${{ env.QT_VERSION }}\msvc2019_64" -DDepsPath="${{ github.workspace }}\cmbuild\deps\win64" -DCOPIED_DEPENDENCIES=NO -DCOPY_DEPENDENCIES=YES -DBUILD_BROWSER=OFF ..
- name: 'Build OBS-Studio 64-bit'
if: steps.build-cache-obs-64.outputs.cache-hit != 'true'
working-directory: ${{ github.workspace }}/obs-studio
run: |
msbuild /m /p:Configuration=RelWithDebInfo .\build64\libobs\libobs.vcxproj
msbuild /m /p:Configuration=RelWithDebInfo .\build64\UI\obs-frontend-api\obs-frontend-api.vcxproj
- name: 'Configure obs-websocket 32-bit'
working-directory: ${{ github.workspace }}/obs-websocket
run: |
mkdir .\build32
cd .\build32
cmake -G "${{ env.CMAKE_GENERATOR }}" -A Win32 -DCMAKE_SYSTEM_VERSION="${{ env.CMAKE_SYSTEM_VERSION }}" -DQTDIR="${{ github.workspace }}\cmbuild\QT\${{ env.QT_VERSION }}\msvc2019" -DLibObs_DIR="${{ github.workspace }}\obs-studio\build32\libobs" -DLIBOBS_INCLUDE_DIR="${{ github.workspace }}\obs-studio\libobs" -DLIBOBS_LIB="${{ github.workspace }}\obs-studio\build32\libobs\RelWithDebInfo\obs.lib" -DOBS_FRONTEND_LIB="${{ github.workspace }}\obs-studio\build32\UI\obs-frontend-api\RelWithDebInfo\obs-frontend-api.lib" ..
- name: 'Configure obs-websocket 64-bit'
working-directory: ${{ github.workspace }}/obs-websocket
run: |
mkdir .\build64
cd .\build64
cmake -G "${{ env.CMAKE_GENERATOR }}" -A x64 -DCMAKE_SYSTEM_VERSION="${{ env.CMAKE_SYSTEM_VERSION }}" -DQTDIR="${{ github.workspace }}\cmbuild\QT\${{ env.QT_VERSION }}\msvc2019_64" -DLibObs_DIR="${{ github.workspace }}\obs-studio\build64\libobs" -DLIBOBS_INCLUDE_DIR="${{ github.workspace }}\obs-studio\libobs" -DLIBOBS_LIB="${{ github.workspace }}\obs-studio\build64\libobs\RelWithDebInfo\obs.lib" -DOBS_FRONTEND_LIB="${{ github.workspace }}\obs-studio\build64\UI\obs-frontend-api\RelWithDebInfo\obs-frontend-api.lib" ..
- name: 'Build obs-websocket 32-bit'
working-directory: ${{ github.workspace }}/obs-websocket
run: msbuild /m /p:Configuration=RelWithDebInfo .\build32\obs-websocket.sln
- name: 'Build obs-websocket 64-bit'
working-directory: ${{ github.workspace }}/obs-websocket
run: msbuild /m /p:Configuration=RelWithDebInfo .\build64\obs-websocket.sln
- name: 'Set PR Artifact Filename'
shell: bash
run: |
if [ "${{ env.GIT_TAG }}" ] ; then \
FILENAME="obs-websocket-${{ env.GIT_TAG }}-Windows" ; \
else \
FILENAME="obs-websocket-${{ env.GIT_HASH }}-git-Windows" ; \
fi
echo "WIN_FILENAME=$FILENAME" >> $GITHUB_ENV
- name: 'Package obs-websocket'
working-directory: ${{ github.workspace }}/obs-websocket
run: |
mkdir package
cd package
7z a "${{ env.WIN_FILENAME }}.zip" "..\release\*"
iscc ..\installer\installer-windows.generated.iss /O. /F"${{ env.WIN_FILENAME }}-Installer"
- name: 'Publish ${{ env.WIN_FILENAME }}.zip'
if: success()
uses: actions/upload-artifact@v2-preview
with:
name: 'obs-websocket-${{ env.PACKAGE_VERSION }}-Windows'
path: ${{ github.workspace }}/obs-websocket/package/*.zip
- name: 'Publish ${{ env.WIN_FILENAME }}-Installer.exe'
if: success()
uses: actions/upload-artifact@v2-preview
with:
name: 'obs-websocket-${{ env.PACKAGE_VERSION }}-Windows-Installer'
path: ${{ github.workspace }}/obs-websocket/package/*.exe
ubuntu64:
name: "Linux/Ubuntu 64-bit"
runs-on: [ubuntu-latest]
if: contains(github.event.head_commit.message, '[skip ci]') != true
steps:
- name: 'Checkout obs-websocket'
uses: actions/checkout@v2
with:
path: ${{ github.workspace }}/obs-websocket
submodules: 'recursive'
- name: 'Checkout OBS-Studio'
uses: actions/checkout@v2
with:
repository: obsproject/obs-studio
path: ${{ github.workspace }}/obs-studio
submodules: 'recursive'
- name: 'Get OBS-Studio Git Info'
shell: bash
working-directory: ${{ github.workspace }}/obs-studio
run: |
git fetch --prune --unshallow
echo "OBS_GIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
echo "OBS_GIT_TAG=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
- name: 'Checkout last OBS-Studio release (${{ env.OBS_GIT_TAG }})'
shell: bash
working-directory: ${{ github.workspace }}/obs-studio
run: |
git checkout ${{ env.OBS_GIT_TAG }}
git submodule update
- name: 'Get obs-websocket git info'
working-directory: ${{ github.workspace }}/obs-websocket
run: |
git fetch --prune --unshallow
GIT_HASH=$(git rev-parse --short HEAD)
echo "GIT_HASH=$GIT_HASH" >> $GITHUB_ENV
GIT_TAG=$(git describe --tags --abbrev=0)
echo "GIT_TAG=$GIT_TAG" >> $GITHUB_ENV
if [ "$GIT_TAG" ] ; then \
VERSION="$GIT_TAG" ; \
else \
VERSION="$GIT_HASH-git" ; \
fi
echo "PACKAGE_VERSION=$VERSION" >> $GITHUB_ENV
- name: 'Install prerequisites (Apt)'
shell: bash
run: |
sudo dpkg --add-architecture amd64
sudo apt-get -qq update
sudo apt-get install -y \
build-essential \
checkinstall \
cmake \
libasound2-dev \
libavcodec-dev \
libavdevice-dev \
libavfilter-dev \
libavformat-dev \
libavutil-dev \
libcurl4-openssl-dev \
libfdk-aac-dev \
libfontconfig-dev \
libfreetype6-dev \
libgl1-mesa-dev \
libjack-jackd2-dev \
libjansson-dev \
libluajit-5.1-dev \
libpulse-dev \
libqt5x11extras5-dev \
libspeexdsp-dev \
libswresample-dev \
libswscale-dev \
libudev-dev \
libv4l-dev \
libva-dev \
libvlc-dev \
libx11-dev \
libx264-dev \
libxcb-randr0-dev \
libxcb-shm0-dev \
libxcb-xinerama0-dev \
libxcomposite-dev \
libxinerama-dev \
libmbedtls-dev \
pkg-config \
python3-dev \
qtbase5-dev \
qtbase5-private-dev \
libqt5svg5-dev \
swig \
libxcb-randr0-dev \
libxcb-xfixes0-dev \
libx11-xcb-dev \
libxcb1-dev \
libxss-dev \
- name: 'Configure OBS-Studio'
working-directory: ${{ github.workspace }}/obs-studio
shell: bash
run: |
mkdir ./build
cd ./build
cmake -DDISABLE_PLUGINS=YES -DENABLE_SCRIPTING=NO -DUNIX_STRUCTURE=YES -DCMAKE_INSTALL_PREFIX=/usr ..
- name: 'Build OBS-Studio'
working-directory: ${{ github.workspace }}/obs-studio
shell: bash
run: |
set -e
cd ./build
make -j4 libobs obs-frontend-api
- name: 'Install OBS-Studio'
working-directory: ${{ github.workspace }}/obs-studio
shell: bash
run: |
cd ./build
sudo cp ./libobs/libobs.so /usr/lib
sudo cp ./UI/obs-frontend-api/libobs-frontend-api.so /usr/lib
sudo mkdir -p /usr/include/obs
sudo cp ../UI/obs-frontend-api/obs-frontend-api.h /usr/include/obs/obs-frontend-api.h
- name: 'Configure obs-websocket'
working-directory: ${{ github.workspace }}/obs-websocket
shell: bash
run: |
mkdir ./build
cd ./build
if [ "${{ env.GIT_TAG }}" ] ; then \
cmake -DLIBOBS_INCLUDE_DIR=${{ github.workspace }}/obs-studio/libobs -DCMAKE_INSTALL_PREFIX=/usr -DUSE_UBUNTU_FIX=TRUE -DCMAKE_BUILD_TYPE=Release .. ; \
else \
cmake -DLIBOBS_INCLUDE_DIR=${{ github.workspace }}/obs-studio/libobs -DCMAKE_INSTALL_PREFIX=/usr -DUSE_UBUNTU_FIX=TRUE .. ; \
fi
- name: 'Build obs-websocket'
working-directory: ${{ github.workspace }}/obs-websocket
shell: bash
run: |
set -e
cd ./build
make -j4
- name: 'Set PR Artifact Filename'
shell: bash
run: |
if [ "${{ env.GIT_TAG }}" ] ; then \
FILENAME="obs-websocket-${{ env.GIT_TAG }}-Ubuntu64.deb" ; \
else \
FILENAME="obs-websocket-${{ env.GIT_HASH }}-git-Ubuntu64.deb" ; \
fi
echo "FILENAME=$FILENAME" >> $GITHUB_ENV
- name: 'Package ${{ env.FILENAME }}'
if: success()
working-directory: ${{ github.workspace }}/obs-websocket
shell: bash
run: |
cd ./build
sudo checkinstall -y --type=debian --fstrans=no -nodoc \
--backup=no --deldoc=yes --install=no --pkgname=obs-websocket --pkgversion=${{ env.PACKAGE_VERSION }} \
--pkglicense="GPLv2.0" --maintainer="${{ github.event.pusher.email }}" --pkggroup="video" \
--pkgsource="${{ github.event.repository.html_url }}" \
--requires="obs-studio,libqt5network5,libqt5concurrent5,qt5-image-formats-plugins" \
--pakdir="../package"
sudo chmod ao+r ../package/*
sudo mv ../package/* ../package/${{ env.FILENAME }}
cd -
- name: 'Publish ${{ env.FILENAME }}'
if: success()
uses: actions/upload-artifact@v2-preview
with:
name: 'obs-websocket-${{ env.PACKAGE_VERSION }}-Ubuntu64'
path: '${{ github.workspace }}/obs-websocket/package/*.deb'
name: 'CI Multiplatform Build'
on:
push:
paths-ignore:
- 'docs/**'
branches:
- master
tags:
- '[45].[0-9]+.[0-9]+*'
pull_request:
paths-ignore:
- 'docs/**'
- '**.md'
branches:
- master
jobs:
windows:
name: 'Windows 32/64-bit'
runs-on: [windows-2019]
if: contains(github.event.head_commit.message, '[skip ci]') != true
env:
QT_CACHE_VERSION: '2' # Change whenever updating OBS dependencies URL, in order to force a cache reset
QT_VERSION: '5.15.2'
WINDOWS_DEPS_CACHE_VERSION: '1' # Change whenever updating Qt dependency URL, in order to force a cache reset
WINDOWS_DEPS_VERSION: '2019'
CMAKE_GENERATOR: 'Visual Studio 16 2019'
CMAKE_SYSTEM_VERSION: '10.0'
steps:
- name: 'Add msbuild to PATH'
uses: microsoft/setup-msbuild@v1.0.2
- name: 'Checkout obs-websocket'
uses: actions/checkout@v2
with:
path: ${{ github.workspace }}/obs-websocket
submodules: 'recursive'
- name: 'Checkout OBS Studio'
uses: actions/checkout@v2
with:
repository: obsproject/obs-studio
path: ${{ github.workspace }}/obs-studio
submodules: 'recursive'
- name: 'Get OBS Studio Git Info'
shell: bash
working-directory: ${{ github.workspace }}/obs-studio
run: |
git fetch --prune --unshallow
echo "OBS_GIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
echo "OBS_GIT_TAG=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
- name: 'Checkout last OBS Studio release (${{ env.OBS_GIT_TAG }})'
shell: bash
working-directory: ${{ github.workspace }}/obs-studio
run: |
git checkout ${{ env.OBS_GIT_TAG }}
git submodule update
- name: 'Get obs-websocket Git Info'
shell: bash
working-directory: ${{ github.workspace }}/obs-websocket
run: |
git fetch --prune --unshallow
GIT_HASH=$(git rev-parse --short HEAD)
echo "GIT_HASH=$GIT_HASH" >> $GITHUB_ENV
GIT_TAG=$(git describe --exact-match --tags --abbrev=0) || GIT_TAG=""
echo "GIT_TAG=$GIT_TAG" >> $GITHUB_ENV
if [ "$GIT_TAG" ] ; then \
VERSION="$GIT_TAG" \
VERSION_SUFFIX=$(echo "$GIT_TAG" | cut -c6-20) ; \
else \
VERSION="$GIT_HASH-git" \
VERSION_SUFFIX="-$GIT_HASH-git" ; \
fi
echo "PACKAGE_VERSION=$VERSION" >> $GITHUB_ENV
echo "CMAKE_VERSION_SUFFIX=$VERSION_SUFFIX" >> $GITHUB_ENV
- name: 'Restore Cached Qt'
id: qtcache
uses: actions/cache@v2
with:
path: Qt_${{ env.QT_VERSION }}.7z
key: 'qtdep-${{ env.QT_CACHE_VERSION }} | ${{ runner.os }}'
- name: 'Download Prerequisite: Qt'
if: steps.qtcache.outputs.cache-hit != 'true'
run: |
curl -kLO https://tt2468.net/dl/Qt_${{ env.QT_VERSION }}.7z -f --retry 5 -C -
- name: 'Extract Prerequisite: Qt'
run: |
7z x Qt_${{ env.QT_VERSION }}.7z -o"${{ github.workspace }}\cmbuild\QT"
- name: 'Restore Cached OBS Studio Dependencies'
id: obscache
uses: actions/cache@v2
with:
path: ${{ github.workspace }}\cmbuild\deps\**
key: 'obsdep-${{ env.WINDOWS_DEPS_CACHE_VERSION }} | ${{ runner.os }}'
- name: 'Install Prerequisite: Pre-built OBS Studio dependencies'
if: steps.obscache.outputs.cache-hit != 'true'
run: |
curl -kLO https://cdn-fastly.obsproject.com/downloads/dependencies${{ env.WINDOWS_DEPS_VERSION }}.zip -f --retry 5 -C -
7z x dependencies${{ env.WINDOWS_DEPS_VERSION }}.zip -o"${{ github.workspace }}\cmbuild\deps"
- name: 'Restore OBS Studio 32-bit Build v${{ env.OBS_GIT_TAG }} from Cache'
id: build-cache-obs-32
uses: actions/cache@v2
env:
CACHE_NAME: 'build-cache-obs-32'
with:
path: ${{ github.workspace }}/obs-studio/build32
key: ${{ runner.os }}-${{ env.CACHE_NAME }}-${{ env.OBS_GIT_TAG }}
- name: 'Configure OBS Studio 32-bit'
if: steps.build-cache-obs-32.outputs.cache-hit != 'true'
working-directory: ${{ github.workspace }}/obs-studio
run: |
if(!(Test-Path -Path ".\build32")){New-Item -ItemType directory -Path .\build32}
cd .\build32
cmake -G "${{ env.CMAKE_GENERATOR }}" -A Win32 -DCMAKE_SYSTEM_VERSION="${{ env.CMAKE_SYSTEM_VERSION }}" -DQTDIR="${{ github.workspace }}\cmbuild\QT\${{ env.QT_VERSION }}\msvc2019" -DDepsPath="${{ github.workspace }}\cmbuild\deps\win32" -DCOPIED_DEPENDENCIES=NO -DCOPY_DEPENDENCIES=YES -DBUILD_BROWSER=OFF ..
- name: 'Build OBS Studio 32-bit'
if: steps.build-cache-obs-32.outputs.cache-hit != 'true'
working-directory: ${{ github.workspace }}/obs-studio
run: |
msbuild /m /p:Configuration=RelWithDebInfo .\build32\libobs\libobs.vcxproj
msbuild /m /p:Configuration=RelWithDebInfo .\build32\UI\obs-frontend-api\obs-frontend-api.vcxproj
- name: 'Restore OBS Studio 64-bit Build v${{ env.OBS_GIT_TAG }} from Cache'
id: build-cache-obs-64
uses: actions/cache@v1
env:
CACHE_NAME: 'build-cache-obs-64'
with:
path: ${{ github.workspace }}/obs-studio/build64
key: ${{ runner.os }}-${{ env.CACHE_NAME }}-${{ env.OBS_GIT_TAG }}
- name: 'Configure OBS Studio 64-bit'
if: steps.build-cache-obs-64.outputs.cache-hit != 'true'
working-directory: ${{ github.workspace }}/obs-studio
run: |
if(!(Test-Path -Path ".\build64")){New-Item -ItemType directory -Path .\build64}
cd .\build64
cmake -G "${{ env.CMAKE_GENERATOR }}" -A x64 -DCMAKE_SYSTEM_VERSION="${{ env.CMAKE_SYSTEM_VERSION }}" -DQTDIR="${{ github.workspace }}\cmbuild\QT\${{ env.QT_VERSION }}\msvc2019_64" -DDepsPath="${{ github.workspace }}\cmbuild\deps\win64" -DCOPIED_DEPENDENCIES=NO -DCOPY_DEPENDENCIES=YES -DBUILD_BROWSER=OFF ..
- name: 'Build OBS Studio 64-bit'
if: steps.build-cache-obs-64.outputs.cache-hit != 'true'
working-directory: ${{ github.workspace }}/obs-studio
run: |
msbuild /m /p:Configuration=RelWithDebInfo .\build64\libobs\libobs.vcxproj
msbuild /m /p:Configuration=RelWithDebInfo .\build64\UI\obs-frontend-api\obs-frontend-api.vcxproj
- name: 'Configure obs-websocket 32-bit'
working-directory: ${{ github.workspace }}/obs-websocket
run: |
mkdir .\build32
cd .\build32
cmake -G "${{ env.CMAKE_GENERATOR }}" -A Win32 -DCMAKE_SYSTEM_VERSION="${{ env.CMAKE_SYSTEM_VERSION }}" -DQTDIR="${{ github.workspace }}\cmbuild\QT\${{ env.QT_VERSION }}\msvc2019" -DLibObs_DIR="${{ github.workspace }}\obs-studio\build32\libobs" -DLIBOBS_INCLUDE_DIR="${{ github.workspace }}\obs-studio\libobs" -DLIBOBS_LIB="${{ github.workspace }}\obs-studio\build32\libobs\RelWithDebInfo\obs.lib" -DOBS_FRONTEND_LIB="${{ github.workspace }}\obs-studio\build32\UI\obs-frontend-api\RelWithDebInfo\obs-frontend-api.lib" -DOBS_WEBSOCKET_VERSION_SUFFIX="${{ env.CMAKE_VERSION_SUFFIX }}" ..
- name: 'Configure obs-websocket 64-bit'
working-directory: ${{ github.workspace }}/obs-websocket
run: |
mkdir .\build64
cd .\build64
cmake -G "${{ env.CMAKE_GENERATOR }}" -A x64 -DCMAKE_SYSTEM_VERSION="${{ env.CMAKE_SYSTEM_VERSION }}" -DQTDIR="${{ github.workspace }}\cmbuild\QT\${{ env.QT_VERSION }}\msvc2019_64" -DLibObs_DIR="${{ github.workspace }}\obs-studio\build64\libobs" -DLIBOBS_INCLUDE_DIR="${{ github.workspace }}\obs-studio\libobs" -DLIBOBS_LIB="${{ github.workspace }}\obs-studio\build64\libobs\RelWithDebInfo\obs.lib" -DOBS_FRONTEND_LIB="${{ github.workspace }}\obs-studio\build64\UI\obs-frontend-api\RelWithDebInfo\obs-frontend-api.lib" -DOBS_WEBSOCKET_VERSION_SUFFIX="${{ env.CMAKE_VERSION_SUFFIX }}" ..
- name: 'Build obs-websocket 32-bit'
working-directory: ${{ github.workspace }}/obs-websocket
run: msbuild /m /p:Configuration=RelWithDebInfo .\build32\obs-websocket.sln
- name: 'Build obs-websocket 64-bit'
working-directory: ${{ github.workspace }}/obs-websocket
run: msbuild /m /p:Configuration=RelWithDebInfo .\build64\obs-websocket.sln
- name: 'Set PR Artifact Filename'
shell: bash
run: |
echo "WIN_FILENAME=obs-websocket-${{ env.PACKAGE_VERSION }}-Windows" >> $GITHUB_ENV
- name: 'Package obs-websocket'
working-directory: ${{ github.workspace }}/obs-websocket
run: |
mkdir package
cd package
7z a "${{ env.WIN_FILENAME }}.zip" "..\release\*"
iscc ..\installer\installer-windows.generated.iss /O. /F"${{ env.WIN_FILENAME }}-Installer"
- name: 'Publish ${{ env.WIN_FILENAME }}.zip'
if: success()
uses: actions/upload-artifact@v2-preview
with:
name: 'obs-websocket-${{ env.PACKAGE_VERSION }}-Windows'
path: ${{ github.workspace }}/obs-websocket/package/*.zip
- name: 'Publish ${{ env.WIN_FILENAME }}-Installer.exe'
if: success()
uses: actions/upload-artifact@v2-preview
with:
name: 'obs-websocket-${{ env.PACKAGE_VERSION }}-Windows-Installer'
path: ${{ github.workspace }}/obs-websocket/package/*.exe
ubuntu64:
name: 'Linux/Ubuntu 64-bit'
runs-on: [ubuntu-latest]
if: contains(github.event.head_commit.message, '[skip ci]') != true
steps:
- name: 'Checkout obs-websocket'
uses: actions/checkout@v2
with:
path: ${{ github.workspace }}/obs-websocket
submodules: 'recursive'
- name: 'Checkout OBS Studio'
uses: actions/checkout@v2
with:
repository: obsproject/obs-studio
path: ${{ github.workspace }}/obs-studio
submodules: 'recursive'
- name: 'Get OBS Studio Git Info'
shell: bash
working-directory: ${{ github.workspace }}/obs-studio
run: |
git fetch --prune --unshallow
echo "OBS_GIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
echo "OBS_GIT_TAG=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
- name: 'Checkout last OBS Studio release (${{ env.OBS_GIT_TAG }})'
shell: bash
working-directory: ${{ github.workspace }}/obs-studio
run: |
git checkout ${{ env.OBS_GIT_TAG }}
git submodule update
- name: 'Get obs-websocket git info'
working-directory: ${{ github.workspace }}/obs-websocket
run: |
git fetch --prune --unshallow
GIT_HASH=$(git rev-parse --short HEAD)
echo "GIT_HASH=$GIT_HASH" >> $GITHUB_ENV
GIT_TAG=$(git describe --exact-match --tags --abbrev=0) || GIT_TAG=""
echo "GIT_TAG=$GIT_TAG" >> $GITHUB_ENV
if [ "$GIT_TAG" ] ; then \
VERSION="$GIT_TAG" \
VERSION_SUFFIX=$(echo "$GIT_TAG" | cut -c6-20) ; \
else \
VERSION="$GIT_HASH-git" \
VERSION_SUFFIX="-$GIT_HASH-git" ; \
fi
echo "PACKAGE_VERSION=$VERSION" >> $GITHUB_ENV
echo "CMAKE_VERSION_SUFFIX=$VERSION_SUFFIX" >> $GITHUB_ENV
- name: 'Install prerequisites (Apt)'
shell: bash
run: |
sudo dpkg --add-architecture amd64
sudo apt-get -qq update
sudo apt-get install -y \
build-essential \
checkinstall \
cmake \
libasound2-dev \
libavcodec-dev \
libavdevice-dev \
libavfilter-dev \
libavformat-dev \
libavutil-dev \
libcurl4-openssl-dev \
libfdk-aac-dev \
libfontconfig-dev \
libfreetype6-dev \
libgl1-mesa-dev \
libjack-jackd2-dev \
libjansson-dev \
libluajit-5.1-dev \
libpulse-dev \
libqt5x11extras5-dev \
libspeexdsp-dev \
libswresample-dev \
libswscale-dev \
libudev-dev \
libv4l-dev \
libva-dev \
libvlc-dev \
libx11-dev \
libx264-dev \
libxcb-randr0-dev \
libxcb-shm0-dev \
libxcb-xinerama0-dev \
libxcomposite-dev \
libxinerama-dev \
libmbedtls-dev \
pkg-config \
python3-dev \
qtbase5-dev \
qtbase5-private-dev \
libqt5svg5-dev \
swig \
libxcb-randr0-dev \
libxcb-xfixes0-dev \
libx11-xcb-dev \
libxcb1-dev \
libxss-dev \
- name: 'Configure OBS Studio'
working-directory: ${{ github.workspace }}/obs-studio
shell: bash
run: |
mkdir ./build
cd ./build
cmake -DDISABLE_PLUGINS=YES -DENABLE_SCRIPTING=NO -DUNIX_STRUCTURE=YES -DCMAKE_INSTALL_PREFIX=/usr ..
- name: 'Build OBS Studio'
working-directory: ${{ github.workspace }}/obs-studio
shell: bash
run: |
set -e
cd ./build
make -j4 libobs obs-frontend-api
- name: 'Install OBS Studio'
working-directory: ${{ github.workspace }}/obs-studio
shell: bash
run: |
cd ./build
sudo cp ./libobs/libobs.so /usr/lib
sudo cp ./UI/obs-frontend-api/libobs-frontend-api.so /usr/lib
sudo mkdir -p /usr/include/obs
sudo cp ../UI/obs-frontend-api/obs-frontend-api.h /usr/include/obs/obs-frontend-api.h
- name: 'Configure obs-websocket'
working-directory: ${{ github.workspace }}/obs-websocket
shell: bash
run: |
mkdir ./build
cd ./build
if [ "${{ env.GIT_TAG }}" ] ; then \
cmake -DLIBOBS_INCLUDE_DIR=${{ github.workspace }}/obs-studio/libobs -DCMAKE_INSTALL_PREFIX=/usr -DUSE_UBUNTU_FIX=TRUE -DOBS_WEBSOCKET_VERSION_SUFFIX="${{ env.CMAKE_VERSION_SUFFIX }}" -DCMAKE_BUILD_TYPE=Release .. ; \
else \
cmake -DLIBOBS_INCLUDE_DIR=${{ github.workspace }}/obs-studio/libobs -DCMAKE_INSTALL_PREFIX=/usr -DUSE_UBUNTU_FIX=TRUE -DOBS_WEBSOCKET_VERSION_SUFFIX="${{ env.CMAKE_VERSION_SUFFIX }}" -DPLUGIN_TESTS=TRUE .. ; \
fi
- name: 'Build obs-websocket'
working-directory: ${{ github.workspace }}/obs-websocket
shell: bash
run: |
set -e
cd ./build
make -j4
- name: 'Set PR Artifact Filename'
shell: bash
run: |
echo "LINUX_FILENAME=obs-websocket-${{ env.PACKAGE_VERSION }}-Ubuntu64.deb" >> $GITHUB_ENV
- name: 'Package ${{ env.LINUX_FILENAME }}'
if: success()
working-directory: ${{ github.workspace }}/obs-websocket
shell: bash
run: |
if [ "${{ env.GIT_TAG }}" ] ; then \
CHECKINSTALL_VERSION="${{ env.PACKAGE_VERSION }}" ; \
else \
CHECKINSTALL_VERSION="1-${{ env.PACKAGE_VERSION }}" ; \
fi
cd ./build
sudo checkinstall -y --type=debian --fstrans=no -nodoc \
--backup=no --deldoc=yes --install=no --pkgname=obs-websocket --pkgversion="$CHECKINSTALL_VERSION" \
--pkglicense="GPLv2.0" --maintainer="${{ github.event.pusher.email }}" --pkggroup="video" \
--pkgsource="${{ github.event.repository.html_url }}" \
--requires="obs-studio,libqt5network5,libqt5concurrent5,qt5-image-formats-plugins" \
--pakdir="../package"
sudo chmod ao+r ../package/*
sudo mv ../package/* ../package/${{ env.LINUX_FILENAME }}
cd -
- name: 'Publish ${{ env.LINUX_FILENAME }}'
if: success()
uses: actions/upload-artifact@v2-preview
with:
name: 'obs-websocket-${{ env.PACKAGE_VERSION }}-Ubuntu64'
path: '${{ github.workspace }}/obs-websocket/package/*.deb'
macOS:
name: 'macOS 64-bit'
runs-on: [macos-latest]
if: contains(github.event.head_commit.message, '[skip ci]') != true
env:
MACOS_DEPS_VERSION: '2022-01-01'
MACOS_DEPS_CACHE_VERSION: '2' # Change whenever updating dependencies version, in order to force a cache reset
steps:
- name: 'Checkout obs-websocket'
uses: actions/checkout@v2
with:
path: ${{ github.workspace }}/obs-websocket
submodules: 'recursive'
- name: 'Checkout OBS Studio'
uses: actions/checkout@v2
with:
repository: obsproject/obs-studio
path: ${{ github.workspace }}/obs-studio
submodules: 'recursive'
- name: 'Install Prerequisite: Binary Signing Certificate'
if: github.event_name != 'pull_request'
uses: apple-actions/import-codesign-certs@v1
with:
p12-file-base64: ${{ secrets.MACOS_SIGNING_CERT }}
p12-password: ${{ secrets.MACOS_SIGNING_CERT_PASSWORD }}
create-keychain: true
keychain-password: ${{ secrets.MACOS_TEMP_CI_KEYCHAIN_PASSWORD }}
- name: 'Install Prerequisite: Installer Signing Certificate'
if: github.event_name != 'pull_request'
uses: apple-actions/import-codesign-certs@v1
with:
p12-file-base64: ${{ secrets.MACOS_INSTALLER_CERT }}
p12-password: ${{ secrets.MACOS_INSTALLER_CERT_PASSWORD }}
create-keychain: false
keychain-password: ${{ secrets.MACOS_TEMP_CI_KEYCHAIN_PASSWORD }}
- name: 'Get OBS Studio Git Info'
shell: bash
working-directory: ${{ github.workspace }}/obs-studio
run: |
git fetch --prune --unshallow
echo "OBS_GIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
echo "OBS_GIT_TAG=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
- name: 'Checkout last OBS Studio release (${{ env.OBS_GIT_TAG }})'
shell: bash
working-directory: ${{ github.workspace }}/obs-studio
run: |
git checkout ${{ env.OBS_GIT_TAG }}
git submodule update
- name: 'Get obs-websocket git info'
shell: bash
working-directory: ${{ github.workspace }}/obs-websocket
run: |
git fetch --prune --unshallow
GIT_HASH=$(git rev-parse --short HEAD)
echo "GIT_HASH=$GIT_HASH" >> $GITHUB_ENV
GIT_TAG=$(git describe --exact-match --tags --abbrev=0) || GIT_TAG=""
echo "GIT_TAG=$GIT_TAG" >> $GITHUB_ENV
if [ "$GIT_TAG" ] ; then \
VERSION="$GIT_TAG" \
VERSION_SUFFIX=$(echo "$GIT_TAG" | cut -c6-20) ; \
else \
VERSION="$GIT_HASH-git" \
VERSION_SUFFIX="-$GIT_HASH-git" ; \
fi
echo "PACKAGE_VERSION=$VERSION" >> $GITHUB_ENV
echo "CMAKE_VERSION_SUFFIX=$VERSION_SUFFIX" >> $GITHUB_ENV
- name: 'Install Packages'
shell: bash
run: |
curl -L -O http://s.sudre.free.fr/Software/files/Packages.dmg
sudo hdiutil attach ./Packages.dmg
sudo installer -pkg /Volumes/Packages\ 1.2.10/Install\ Packages.pkg -target /
- name: 'Restore Cached Qt & OBS Studio dependencies'
id: deps-cache
uses: actions/cache@v2
with:
path: ${{ github.workspace }}/obsdeps/**
key: 'deps-cache-${{ env.MACOS_DEPS_CACHE_VERSION }} | ${{ runner.os }}'
- name: 'Install Prerequisite: Qt + OBS Studio dependencies'
if: steps.deps-cache.outputs.cache-hit != 'true'
shell: bash
run: |
mkdir -p obsdeps
curl -L -O https://github.com/obsproject/obs-deps/releases/download/${{ env.MACOS_DEPS_VERSION }}/macos-deps-qt-${{ env.MACOS_DEPS_VERSION }}-universal.tar.xz
tar -xf macos-deps-qt-${{ env.MACOS_DEPS_VERSION }}-universal.tar.xz -C "./obsdeps"
curl -L -O https://github.com/obsproject/obs-deps/releases/download/${{ env.MACOS_DEPS_VERSION }}/macos-deps-${{ env.MACOS_DEPS_VERSION }}-universal.tar.xz
tar -xf macos-deps-${{ env.MACOS_DEPS_VERSION }}-universal.tar.xz -C "./obsdeps"
- run: xattr -r -d com.apple.quarantine ./obsdeps
shell: bash
- name: 'Configue OBS Studio'
if: steps.cache-obs-build.outputs.cache-hit != 'true'
working-directory: ${{ github.workspace }}/obs-studio
shell: bash
run: |
mkdir -p ./build
cd ./build
cmake .. \
-DQTDIR=${{ github.workspace }}/obsdeps \
-DDepsPath=${{ github.workspace }}/obsdeps \
-DCMAKE_OSX_DEPLOYMENT_TARGET=10.13 \
-DDISABLE_PLUGINS=true \
-DENABLE_SCRIPTING=0 \
-DCMAKE_PREFIX_PATH=${{ github.workspace }}/obsdeps/lib/cmake
- name: 'Build OBS Studio'
if: steps.cache-obs-build.outputs.cache-hit != 'true'
working-directory: ${{ github.workspace }}/obs-studio/build
shell: bash
run: |
set -e
make -j4 libobs obs-frontend-api
- name: 'Configure obs-websocket'
working-directory: ${{ github.workspace }}/obs-websocket
shell: bash
run: |
mkdir -p build
cd build
cmake .. \
-DCMAKE_OSX_DEPLOYMENT_TARGET=10.13 \
-DQTDIR=${{ github.workspace }}/obsdeps \
-DLIBOBS_INCLUDE_DIR=${{ github.workspace }}/obs-studio/libobs \
-DLIBOBS_LIB=${{ github.workspace }}/obs-studio/libobs \
-DOBS_FRONTEND_LIB="${{ github.workspace }}/obs-studio/build/UI/obs-frontend-api/libobs-frontend-api.dylib" \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DCMAKE_INSTALL_PREFIX=/usr \
-DOBS_WEBSOCKET_VERSION_SUFFIX="${{ env.CMAKE_VERSION_SUFFIX }}"
- name: 'Build obs-websocket'
working-directory: ${{ github.workspace }}/obs-websocket/build
shell: bash
run: |
set -e
make -j4
- name: 'Relink Qt'
shell: bash
working-directory: ${{ github.workspace }}/obs-websocket/build
run: |
install_name_tool \
-change /tmp/obsdeps/lib/QtWidgets.framework/Versions/5/QtWidgets \
@executable_path/../Frameworks/QtWidgets.framework/Versions/5/QtWidgets \
-change /tmp/obsdeps/lib/QtGui.framework/Versions/5/QtGui \
@executable_path/../Frameworks/QtGui.framework/Versions/5/QtGui \
-change /tmp/obsdeps/lib/QtCore.framework/Versions/5/QtCore \
@executable_path/../Frameworks/QtCore.framework/Versions/5/QtCore \
-change /tmp/obsdeps/lib/QtNetwork.framework/Versions/5/QtNetwork \
@executable_path/../Frameworks/QtNetwork.framework/Versions/5/QtNetwork \
-change /tmp/obsdeps/lib/QtSvg.framework/Versions/5/QtSvg \
@executable_path/../Frameworks/QtSvg.framework/Versions/5/QtSvg \
./obs-websocket.so
- name: 'Sign plugin binary'
if: github.event_name != 'pull_request'
shell: bash
working-directory: ${{ github.workspace }}/obs-websocket/build
run: |
codesign --sign "${{ secrets.MACOS_SIGNING_IDENTITY }}" ./obs-websocket.so
- name: 'Set PR Artifact Filename'
shell: bash
run: |
echo "MACOS_FILENAME=obs-websocket-${{ env.PACKAGE_VERSION }}-macOS.pkg" >> $GITHUB_ENV
echo "MACOS_FILENAME_UNSIGNED=obs-websocket-${{ env.PACKAGE_VERSION }}-macOS-Unsigned.pkg" >> $GITHUB_ENV
- name: 'Package ${{ env.MACOS_FILENAME_UNSIGNED }}'
if: success()
working-directory: ${{ github.workspace }}/obs-websocket
shell: bash
run: |
packagesbuild ./CI/macos/obs-websocket.pkgproj
mv ./release/obs-websocket.pkg ./release/${{ env.MACOS_FILENAME_UNSIGNED }}
- name: 'Sign plugin package'
if: ${{ env.GIT_TAG != '' }}
shell: bash
working-directory: ${{ github.workspace }}/obs-websocket
run: |
productsign \
--sign "${{ secrets.MACOS_INSTALLER_IDENTITY }}" \
./release/${{ env.MACOS_FILENAME_UNSIGNED }} \
./release/${{ env.MACOS_FILENAME }}
rm ./release/${{ env.MACOS_FILENAME_UNSIGNED }}
- name: 'Notarize package'
if: ${{ env.GIT_TAG != '' }}
shell: bash
working-directory: ${{ github.workspace }}/obs-websocket
run: |
zip -r ./release/${{ env.MACOS_FILENAME }}.zip ./release/${{ env.MACOS_FILENAME }}
UPLOAD_RESULT=$(xcrun altool --notarize-app \
--primary-bundle-id "com.obsproject.obs-websocket" \
--username "${{ secrets.MACOS_NOTARIZATION_USERNAME }}" \
--password "${{ secrets.MACOS_NOTARIZATION_PASSWORD }}" \
--asc-provider "${{ secrets.ASC_PROVIDER_SHORTNAME }}" \
--file "./release/${{ env.MACOS_FILENAME }}.zip")
rm ./release/${{ env.MACOS_FILENAME }}.zip
REQUEST_UUID=$(echo $UPLOAD_RESULT | awk -F ' = ' '/RequestUUID/ {print $2}')
# Pieces of code borrowed from rednoah/notarized-app
while sleep 30 && date; do
CHECK_RESULT=$(xcrun altool \
--notarization-info "$REQUEST_UUID" \
--username "${{ secrets.MACOS_NOTARIZATION_USERNAME }}" \
--password "${{ secrets.MACOS_NOTARIZATION_PASSWORD }}" \
--asc-provider "${{ secrets.ASC_PROVIDER_SHORTNAME }}")
if ! grep -q "Status: in progress" <<< "$CHECK_RESULT"; then
xcrun stapler staple ./release/${{ env.MACOS_FILENAME }}
break
fi
done
- name: 'Publish Packages'
if: success()
uses: actions/upload-artifact@v2-preview
with:
name: 'obs-websocket-${{ env.PACKAGE_VERSION }}-macOS'
path: '${{ github.workspace }}/obs-websocket/release/*.pkg'

3
.gitignore vendored
View File

@ -9,3 +9,6 @@
.idea
.vscode
/docs/node_modules/
/src/plugin-macros.generated.h
/installer/installer-windows.generated.iss
/cmake-build-debug/

View File

@ -1,66 +0,0 @@
# Compiling obs-websocket
## Prerequisites
You'll need [Qt 5.15.2 on Windows/Mac](https://download.qt.io/official_releases/qt/5.10/) or Qt 5.12.8 on Linux,
[CMake](https://cmake.org/download/) and a working [OBS Studio development environment](https://obsproject.com/wiki/install-instructions) installed on your
computer.
## Windows
In cmake-gui, you'll have to set the following variables :
- **QTDIR** (path) : location of the Qt environment suited for your compiler and architecture
- **LIBOBS_INCLUDE_DIR** (path) : location of the libobs subfolder in the source code of OBS Studio
- **LIBOBS_LIB** (filepath) : location of the obs.lib file
- **OBS_FRONTEND_LIB** (filepath) : location of the obs-frontend-api.lib file
## Linux
On Debian/Ubuntu :
```shell
sudo apt-get install libboost-all-dev
git clone --recursive https://github.com/Palakis/obs-websocket.git
cd obs-websocket
mkdir build && cd build
cmake -DLIBOBS_INCLUDE_DIR="<path to the libobs sub-folder in obs-studio's source code>" -DCMAKE_INSTALL_PREFIX=/usr -DUSE_UBUNTU_FIX=true ..
make -j4
sudo make install
```
On other linux OS's, use this cmake command instead:
```shell
cmake -DLIBOBS_INCLUDE_DIR="<path to the libobs sub-folder in obs-studio's source code>" -DCMAKE_INSTALL_PREFIX=/usr ..
```
## OS X
As a prerequisite, you will need Xcode for your current OSX version, the Xcode command line tools, and [Homebrew](https://brew.sh/).
Homebrew's setup will guide you in getting your system set up, you should be good to go once Homebrew is successfully up and running.
Use of the macOS CI scripts is recommended. Please note that these
scripts install new software and can change several settings on your system. An
existing obs-studio development environment is not required, as
`install-build-obs-macos.sh` will install it for you. If you already have a
working obs-studio development environment and have built obs-studio, you can
skip that script.
Of course, you're encouraged to dig through the contents of these scripts to
look for issues or specificities.
```shell
git clone --recursive https://github.com/Palakis/obs-websocket.git
cd obs-websocket
./CI/install-dependencies-macos.sh
./CI/install-build-obs-macos.sh
./CI/build-macos.sh
./CI/package-macos.sh
```
This will result in a ready-to-use `obs-websocket.pkg` installer in the `release` subfolder.
## Automated Builds
![Github Actions](https://github.com/Palakis/obs-websocket/actions/workflows/main.yml/badge.svg?branch=master)

View File

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

View File

@ -1,5 +0,0 @@
brew "jack"
brew "speexdsp"
brew "cmake"
brew "freetype"
brew "fdk-aac"

View File

@ -1,26 +0,0 @@
#!/bin/sh
OSTYPE=$(uname)
if [ "${OSTYPE}" != "Darwin" ]; then
echo "[obs-websocket - Error] macOS build script can be run on Darwin-type OS only."
exit 1
fi
HAS_CMAKE=$(type cmake 2>/dev/null)
if [ "${HAS_CMAKE}" = "" ]; then
echo "[obs-websocket - Error] CMake not installed - please run 'install-dependencies-macos.sh' first."
exit 1
fi
echo "[obs-websocket] Building 'obs-websocket' for macOS."
mkdir -p build && cd build
cmake .. \
-DQTDIR=/tmp/obsdeps \
-DLIBOBS_INCLUDE_DIR=../../obs-studio/libobs \
-DLIBOBS_LIB=../../obs-studio/libobs \
-DOBS_FRONTEND_LIB="$(pwd)/../../obs-studio/build/UI/obs-frontend-api/libobs-frontend-api.dylib" \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DCMAKE_INSTALL_PREFIX=/usr \
&& make -j4

View File

@ -1,39 +0,0 @@
#!/bin/sh
OSTYPE=$(uname)
if [ "${OSTYPE}" != "Darwin" ]; then
echo "[obs-websocket - Error] macOS obs-studio build script can be run on Darwin-type OS only."
exit 1
fi
HAS_CMAKE=$(type cmake 2>/dev/null)
HAS_GIT=$(type git 2>/dev/null)
if [ "${HAS_CMAKE}" = "" ]; then
echo "[obs-websocket - Error] CMake not installed - please run 'install-dependencies-macos.sh' first."
exit 1
fi
if [ "${HAS_GIT}" = "" ]; then
echo "[obs-websocket - Error] Git not installed - please install Xcode developer tools or via Homebrew."
exit 1
fi
# Build obs-studio
cd ..
echo "[obs-websocket] Cloning obs-studio from GitHub.."
git clone https://github.com/obsproject/obs-studio
cd obs-studio
OBSLatestTag=$(git describe --tags --abbrev=0)
git checkout $OBSLatestTag
mkdir build && cd build
echo "[obs-websocket] Building obs-studio.."
cmake .. \
-DQTDIR=/tmp/obsdeps \
-DDepsPath=/tmp/obsdeps \
-DCMAKE_OSX_DEPLOYMENT_TARGET=10.13 \
-DDISABLE_PLUGINS=true \
-DENABLE_SCRIPTING=0 \
-DCMAKE_PREFIX_PATH=/tmp/obsdeps/lib/cmake \
&& make -j4

View File

@ -1,57 +0,0 @@
#!/bin/sh
OSTYPE=$(uname)
if [ "${OSTYPE}" != "Darwin" ]; then
echo "[obs-websocket - Error] macOS install dependencies script can be run on Darwin-type OS only."
exit 1
fi
HAS_BREW=$(type brew 2>/dev/null)
if [ "${HAS_BREW}" = "" ]; then
echo "[obs-websocket - Error] Please install Homebrew (https://www.brew.sh/) to build obs-websocket on macOS."
exit 1
fi
# OBS Studio Brew Deps
echo "[obs-websocket] Updating Homebrew.."
brew update >/dev/null
echo "[obs-websocket] Checking installed Homebrew formulas.."
if [ -d /usr/local/opt/openssl@1.0.2t ]; then
brew uninstall openssl@1.0.2t
brew untap local/openssl
fi
if [ -d /usr/local/opt/python@2.7.17 ]; then
brew uninstall python@2.7.17
brew untap local/python2
fi
brew bundle --file ./CI/macos/Brewfile
# Fetch and install Packages app
# =!= NOTICE =!=
# Installs a LaunchDaemon under /Library/LaunchDaemons/fr.whitebox.packages.build.dispatcher.plist
# =!= NOTICE =!=
HAS_PACKAGES=$(type packagesbuild 2>/dev/null)
if [ "${HAS_PACKAGES}" = "" ]; then
echo "[obs-websocket] Installing Packaging app (might require password due to 'sudo').."
curl -L -O http://s.sudre.free.fr/Software/files/Packages.dmg
sudo hdiutil attach ./Packages.dmg
sudo installer -pkg /Volumes/Packages\ 1.2.9/Install\ Packages.pkg -target /
fi
# OBS Deps
echo "[obs-websocket] Installing obs-websocket dependency 'OBS Deps ${OBS_DEPS_VERSION}'.."
wget --quiet --retry-connrefused --waitretry=1 https://github.com/obsproject/obs-deps/releases/download/${OBS_DEPS_VERSION}/macos-deps-${OBS_DEPS_VERSION}.tar.gz
tar -xf ./macos-deps-${OBS_DEPS_VERSION}.tar.gz -C /tmp
# Qt deps
echo "[obs-websocket] Installing obs-websocket dependency 'Qt ${QT_VERSION}'.."
curl -L -O https://github.com/obsproject/obs-deps/releases/download/${OBS_DEPS_VERSION}/macos-qt-${QT_VERSION}-${OBS_DEPS_VERSION}.tar.gz
tar -xf ./macos-qt-${QT_VERSION}-${OBS_DEPS_VERSION}.tar.gz -C "/tmp"
xattr -r -d com.apple.quarantine /tmp/obsdeps

View File

@ -514,7 +514,7 @@
<key>CONCLUSION_ACTION</key>
<integer>0</integer>
<key>IDENTIFIER</key>
<string>fr.palakis.obs-websocket</string>
<string>com.obsproject.obs-websocket</string>
<key>OVERWRITE_PERMISSIONS</key>
<false/>
<key>VERSION</key>

View File

@ -1,89 +0,0 @@
#!/bin/bash
set -e
OSTYPE=$(uname)
if [ "${OSTYPE}" != "Darwin" ]; then
echo "[obs-websocket - Error] macOS package script can be run on Darwin-type OS only."
exit 1
fi
echo "[obs-websocket] Preparing package build"
GIT_HASH=$(git rev-parse --short HEAD)
GIT_BRANCH_OR_TAG=$(git name-rev --name-only HEAD | awk -F/ '{print $NF}')
VERSION="$GIT_HASH-$GIT_BRANCH_OR_TAG"
FILENAME_UNSIGNED="obs-websocket-$VERSION-Unsigned.pkg"
FILENAME="obs-websocket-$VERSION.pkg"
echo "[obs-websocket] Modifying obs-websocket.so linking"
install_name_tool \
-change /tmp/obsdeps/lib/QtWidgets.framework/Versions/5/QtWidgets \
@executable_path/../Frameworks/QtWidgets.framework/Versions/5/QtWidgets \
-change /tmp/obsdeps/lib/QtGui.framework/Versions/5/QtGui \
@executable_path/../Frameworks/QtGui.framework/Versions/5/QtGui \
-change /tmp/obsdeps/lib/QtCore.framework/Versions/5/QtCore \
@executable_path/../Frameworks/QtCore.framework/Versions/5/QtCore \
./build/obs-websocket.so
# Check if replacement worked
echo "[obs-websocket] Dependencies for obs-websocket"
otool -L ./build/obs-websocket.so
if [[ "$RELEASE_MODE" == "True" ]]; then
echo "[obs-websocket] Signing plugin binary: obs-websocket.so"
codesign --sign "$CODE_SIGNING_IDENTITY" ./build/obs-websocket.so
else
echo "[obs-websocket] Skipped plugin codesigning"
fi
echo "[obs-websocket] Actual package build"
packagesbuild ./CI/macos/obs-websocket.pkgproj
echo "[obs-websocket] Renaming obs-websocket.pkg to $FILENAME"
mv ./release/obs-websocket.pkg ./release/$FILENAME_UNSIGNED
if [[ "$RELEASE_MODE" == "True" ]]; then
echo "[obs-websocket] Signing installer: $FILENAME"
productsign \
--sign "$INSTALLER_SIGNING_IDENTITY" \
./release/$FILENAME_UNSIGNED \
./release/$FILENAME
rm ./release/$FILENAME_UNSIGNED
echo "[obs-websocket] Submitting installer $FILENAME for notarization"
zip -r ./release/$FILENAME.zip ./release/$FILENAME
UPLOAD_RESULT=$(xcrun altool \
--notarize-app \
--primary-bundle-id "fr.palakis.obs-websocket" \
--username "$AC_USERNAME" \
--password "$AC_PASSWORD" \
--asc-provider "$AC_PROVIDER_SHORTNAME" \
--file "./release/$FILENAME.zip")
rm ./release/$FILENAME.zip
REQUEST_UUID=$(echo $UPLOAD_RESULT | awk -F ' = ' '/RequestUUID/ {print $2}')
echo "Request UUID: $REQUEST_UUID"
echo "[obs-websocket] Wait for notarization result"
# Pieces of code borrowed from rednoah/notarized-app
while sleep 30 && date; do
CHECK_RESULT=$(xcrun altool \
--notarization-info "$REQUEST_UUID" \
--username "$AC_USERNAME" \
--password "$AC_PASSWORD" \
--asc-provider "$AC_PROVIDER_SHORTNAME")
echo $CHECK_RESULT
if ! grep -q "Status: in progress" <<< "$CHECK_RESULT"; then
echo "[obs-websocket] Staple ticket to installer: $FILENAME"
xcrun stapler staple ./release/$FILENAME
break
fi
done
else
echo "[obs-websocket] Skipped installer codesigning and notarization"
fi

View File

@ -1,10 +1,23 @@
cmake_minimum_required(VERSION 3.16...3.20)
# Version variables
project(obs-websocket VERSION 5.0.0)
set(OBS_WEBSOCKET_RPC_VERSION 1)
# Set correct version string
if(DEFINED OBS_WEBSOCKET_VERSION_SUFFIX AND NOT OBS_WEBSOCKET_VERSION_SUFFIX STREQUAL "")
set(OBS_WEBSOCKET_VERSION "${CMAKE_PROJECT_VERSION}${OBS_WEBSOCKET_VERSION_SUFFIX}")
message(WARNING "-----------------------------------\nVersion Suffix provided. OBS_WEBSOCKET_VERSION is now ${OBS_WEBSOCKET_VERSION}\n-----------------------------------")
else()
set(OBS_WEBSOCKET_VERSION "${CMAKE_PROJECT_VERSION}")
endif()
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Prohibit in-source builds
file(TO_CMAKE_PATH "${PROJECT_BINARY_DIR}/CMakeLists.txt" _LOC_PATH)
if(EXISTS "${LOC_PATH}")
@ -23,6 +36,10 @@ if(NOT CMAKE_BUILD_TYPE)
endif()
# Plugin tests flag
option(PLUGIN_TESTS "Enable plugin runtime tests" OFF)
# Qt build stuff
set(CMAKE_PREFIX_PATH "${QTDIR}")
set(CMAKE_INCLUDE_CURRENT_DIR ON)
@ -49,7 +66,7 @@ find_package(LibObs REQUIRED)
# Find Qt5
find_package(Qt5 REQUIRED COMPONENTS Core Widgets Svg Concurrent Network)
find_package(Qt5 REQUIRED COMPONENTS Core Widgets Svg Network)
# Find nlohmann
@ -72,9 +89,10 @@ configure_file(
set(obs-websocket_SOURCES
src/obs-websocket.cpp
src/Config.cpp
src/WebSocketServer.cpp
src/WebSocketProtocol.cpp
src/WebSocketSession.cpp
src/WebSocketApi.cpp
src/websocketserver/WebSocketServer.cpp
src/websocketserver/WebSocketServer_Protocol.cpp
src/websocketserver/rpc/WebSocketSession.cpp
src/eventhandler/EventHandler.cpp
src/eventhandler/EventHandler_General.cpp
src/eventhandler/EventHandler_Config.cpp
@ -85,14 +103,24 @@ set(obs-websocket_SOURCES
src/eventhandler/EventHandler_Outputs.cpp
src/eventhandler/EventHandler_SceneItems.cpp
src/eventhandler/EventHandler_MediaInputs.cpp
src/eventhandler/EventHandler_Ui.cpp
src/requesthandler/RequestHandler.cpp
src/requesthandler/RequestBatchHandler.cpp
src/requesthandler/RequestHandler_General.cpp
src/requesthandler/RequestHandler_Config.cpp
src/requesthandler/RequestHandler_Sources.cpp
src/requesthandler/RequestHandler_Scenes.cpp
src/requesthandler/RequestHandler_Inputs.cpp
src/requesthandler/RequestHandler_Transitions.cpp
src/requesthandler/RequestHandler_Filters.cpp
src/requesthandler/RequestHandler_SceneItems.cpp
src/requesthandler/RequestHandler_Outputs.cpp
src/requesthandler/RequestHandler_Stream.cpp
src/requesthandler/RequestHandler_Record.cpp
src/requesthandler/RequestHandler_MediaInputs.cpp
src/requesthandler/RequestHandler_Ui.cpp
src/requesthandler/rpc/Request.cpp
src/requesthandler/rpc/RequestBatchRequest.cpp
src/requesthandler/rpc/RequestResult.cpp
src/forms/SettingsDialog.cpp
src/forms/ConnectInfo.cpp
@ -100,28 +128,46 @@ set(obs-websocket_SOURCES
src/utils/Crypto.cpp
src/utils/Json.cpp
src/utils/Obs.cpp
src/utils/Obs_StringHelper.cpp
src/utils/Obs_EnumHelper.cpp
src/utils/Obs_NumberHelper.cpp
src/utils/Obs_ArrayHelper.cpp
src/utils/Obs_ObjectHelper.cpp
src/utils/Obs_SearchHelper.cpp
src/utils/Obs_ActionHelper.cpp
src/utils/Obs_VolumeMeter.cpp
src/utils/Platform.cpp
src/utils/Compat.cpp
deps/qr/cpp/QrCode.cpp)
set(obs-websocket_HEADERS
src/obs-websocket.h
src/Config.h
src/WebSocketServer.h
src/WebSocketProtocol.h
src/WebSocketSession.h
src/WebSocketApi.h
src/websocketserver/WebSocketServer.h
src/websocketserver/types/WebSocketCloseCode.h
src/websocketserver/types/WebSocketOpCode.h
src/websocketserver/rpc/WebSocketSession.h
src/eventhandler/EventHandler.h
src/eventhandler/types/EventSubscription.h
src/requesthandler/RequestHandler.h
src/requesthandler/RequestBatchHandler.h
src/requesthandler/types/RequestStatus.h
src/requesthandler/types/RequestBatchExecutionType.h
src/requesthandler/rpc/Request.h
src/requesthandler/rpc/RequestBatchRequest.h
src/requesthandler/rpc/RequestResult.h
src/requesthandler/rpc/RequestStatus.h
src/forms/SettingsDialog.h
src/forms/ConnectInfo.h
src/utils/Crypto.h
src/utils/Json.h
src/utils/Obs.h
src/utils/Obs_VolumeMeter.h
src/utils/Obs_VolumeMeter_Helpers.h
src/utils/Platform.h
src/utils/Compat.h
src/utils/Utils.h
lib/obs-websocket-api.h
deps/qr/cpp/QrCode.hpp)
@ -135,7 +181,6 @@ include_directories(
${Qt5Core_INCLUDES}
${Qt5Widgets_INCLUDES}
${Qt5Svg_INCLUDES}
${Qt5Concurrent_INCLUDES}
${Qt5Network_INCLUDES}
"${CMAKE_SOURCE_DIR}/deps/asio/asio/include"
"${CMAKE_SOURCE_DIR}/deps/websocketpp")
@ -145,10 +190,13 @@ target_link_libraries(obs-websocket
Qt5::Core
Qt5::Widgets
Qt5::Svg
Qt5::Concurrent
Qt5::Network
nlohmann_json::nlohmann_json)
if(PLUGIN_TESTS)
target_compile_definitions(obs-websocket PRIVATE PLUGIN_TESTS)
endif()
# Windows-specific build settings and tasks
if(WIN32)
@ -256,6 +304,8 @@ endif()
if(UNIX AND NOT APPLE)
include(GNUInstallDirs)
target_compile_options(obs-websocket PRIVATE -Wall -Wextra -Wpedantic -Werror -Wno-missing-field-initializers)
set_target_properties(obs-websocket PROPERTIES PREFIX "")
target_link_libraries(obs-websocket obs-frontend-api)

View File

@ -1,88 +0,0 @@
# Contributing to obs-websocket
## Translating obs-websocket to your language
Localization happens on [Crowdin](https://crowdin.com/project/obs-websocket)
## Branches
**Development happens on `master`**
## Writing code for obs-websocket
### Code Formatting Guidelines
* Function and variable names: camelCase for variables, MixedCaps for method names
* Request and Event names should use MixedCaps names. Keep naming conformity of request naming using similar terms like `Get`, `Set`, `Get[x]List`, `Start[x]`, `Toggle[x]`.
* Request and Event json properties/fields should use camelCase. Try to use existing property names.
* Code is indented with Tabs. Assume they are 4 columns wide
* 80 columns max code width. (Comments/docs can be larger)
* New and updated requests/events must always come with accompanying documentation comments (see existing protocol elements for examples).
These are required to automatically generate the [protocol specification document](docs/generated/protocol.md).
### Code Best-Practices
* Favor return-early code and avoid wrapping huge portions of code in conditionals. As an example, this:
```cpp
if (success) {
return RequestResult::Success();
} else {
return RequestResult::Error(RequestStatus::GenericError);
}
```
is better like this:
```cpp
if (!success) {
return RequestResult::Error(RequestStatus::GenericError);
}
return RequestResult::Success();
```
* Try to use the [built-in](https://github.com/Palakis/obs-websocket/blob/master/src/requesthandler/rpc/Request.h) request checks when possible.
* Refer to existing requests for usage examples.
* Some example common response/request property names are:
* `sceneName` - The name of a scene
* `inputName` - The name of an input
* `sourceName` - The name of a source (only for when multiple source types apply)
* `sceneItemEnabled` - Whether a scene item is enabled
* Response parameters which have no attributed data due to an invalid state should be set to `null` (versus being left out)
* For example, when `GetSceneList` is called and OBS is not in studio mode, `currentPreviewSceneName` will be `null`
* If a request's core response data depends on a state, an error should be thrown unless `ignoreNonFatalRequestChecks` is set. See `GetCurrentPreviewScene` as an example.
### Commit Guidelines
* Commits follow the 50/72 standard:
* 50 characters max for the commit title (excluding scope name)
* One empty line after the title
* Description wrapped to 72 columns max width per line.
* Commit titles:
* Use present tense
* Prefix the title with a "scope" name
* e.g: "CI: fix wrong behaviour when packaging for OS X"
* Typical scopes: CI, General, Requests, Events, Server
**Example commit:**
```
Requests: Add GetSceneList
Adds a new request called `GetSceneList` which returns the current
scene, along with an array of objects, each one with a scene name
and index.
```
### Pull Requests
* Pull Requests must never be based off your fork's main branch (in this case, `master`).
* Start your work in a newly named branch based on the upstream main one (e.g.: `feature/cool-new-feature`, `bugfix/fix-palakis-mistakes`, ...)
* If your work is not done yet, but for any reason you need to PR it (like collecting discussions, testing with CI, getting testers),
create it as a Draft Pull Request (open the little arrow menu next to the "Create pull request" button, then select "Create draft pull request").

View File

@ -1,39 +1,21 @@
# obs-websocket
## YOU HAVE STUMBLED UPON THE DEV BRANCH FOR V5.0.0
- You can find the main protocol spec here: [PROTOCOL.md](docs/generated/protocol.md).
- You can find the planned requests sheet [here](https://docs.google.com/spreadsheets/d/1LfCZrbT8e7cSaKo_TuPDd-CJiptL7RSuo8iE63vMmMs/edit?usp=sharing)
<p align="center">
<img src="/.github/images/obsws_logo.png" width=150 align="center">
</p>
WebSockets API for OBS Studio.
WebSocket API for OBS Studio.
[![CI Multiplatform Build](https://github.com/Palakis/obs-websocket/actions/workflows/main.yml/badge.svg?branch=master)](https://github.com/Palakis/obs-websocket/actions/workflows/main.yml)
[![CI Multiplatform Build](https://github.com/obsproject/obs-websocket/actions/workflows/main.yml/badge.svg?branch=master)](https://github.com/obsproject/obs-websocket/actions/workflows/main.yml)
[![Discord](https://img.shields.io/discord/715691013825364120.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/WBaSQ3A)
[![Financial Contributors on Open Collective](https://opencollective.com/obs-websocket/all/badge.svg?label=financial+contributors)](https://opencollective.com/obs-websocket)
[![Financial Contributors on Open Collective](https://opencollective.com/obs-websocket-dev/all/badge.svg?label=financial+contributors)](https://opencollective.com/obs-websocket-dev)
## Downloads
Binaries for Windows, MacOS, and Linux are available in the [Releases](https://github.com/Palakis/obs-websocket/releases) section.
### Homebrew
If you're using MacOS you can use Homebrew for installation as well:
```sh
brew install obs-websocket
```
Binaries for Windows, MacOS, and Linux are available in the [Releases](https://github.com/obsproject/obs-websocket/releases) section.
## Using obs-websocket
Here is a list of available web clients: (compatible with tablets and other touch interfaces)
- (No known clients supporting 5.0.0)
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.
(Psst. You can use `--websocket_port`(value), `--websocket_password`(value), and `--websocket_debug`(flag) on the command line to override the configured values.)
@ -44,58 +26,47 @@ It is **highly recommended** to protect obs-websocket with a password against un
- Change your stream overlay/graphics based on the current scene
- Automate scene switching with a third-party program (e.g. : auto-pilot, foot pedal, ...)
### For developers
### Client software
- (No known clients supporting 5.0.0 at the moment. Ping us in the Discord if you have one!)
The server is a typical Websockets server running by default on port 4444 (the port number can be changed in the Settings dialog).
The protocol understood by the server is documented in [PROTOCOL.md](docs/generated/protocol.md).
### Client libraries (for developers)
Here's a list of available language APIs for obs-websocket :
- (No known apis supporting 5.0.0)
Here's a list of available language APIs for obs-websocket:
- Python 3.7+ (Asyncio): [simpleobsws](https://github.com/IRLToolkit/simpleobsws/tree/master) by IRLToolkit
- Rust: [obws](https://github.com/dnaka91/obws/tree/v5-api) by dnaka91
- Godot 3.4.x: [obs-websocket-gd](https://github.com/you-win/obs-websocket-gd) by you-win
We'd like to know what you're building with or for obs-websocket. If you do something in this fashion, feel free to drop a message in `#project-showoff` in the [discord server!](https://discord.gg/WBaSQ3A)
The 5.x server is a typical WebSocket server running by default on port 4455 (the port number can be changed in the Settings dialog under `Tools`).
The protocol we use is documented in [PROTOCOL.md](docs/generated/protocol.md).
### Securing obs-websocket (via TLS/SSL)
If you are intending to use obs-websocket outside of a LAN environment, it is highly recommended to secure the connection using a tunneling service.
See the SSL [tunnelling guide](SSL-TUNNELLING.md) for easy instructions on how to encrypt your websocket connection.
## Compiling obs-websocket
See the [build instructions](BUILDING.md).
## Translations
**Your help is welcome on translations.**
Please join the localization project on [Crowdin](https://crowdin.com/project/obs-websocket)
We'd like to know what you're building with obs-websocket! If you do something in this fashion, feel free to drop a message in `#project-showoff` in the [discord server!](https://discord.gg/WBaSQ3A)
## Contributors
### Code Contributors
This project exists thanks to all the people who contribute. [Contribute](CONTRIBUTING.md).
<a href="https://github.com/Palakis/obs-websocket/graphs/contributors"><img src="https://opencollective.com/obs-websocket/contributors.svg?width=890&button=false" /></a>
This project exists thanks to [all the people](graphs/contributors) who contribute. [Contribute](wiki/Contributing-Guidelines).
<a href="https://github.com/obsproject/obs-websocket/graphs/contributors"><img src="https://opencollective.com/obs-websocket-dev/contributors.svg?width=890&button=false" /></a>
### Financial Contributors
Become a financial contributor and help us sustain our community. [Contribute](https://opencollective.com/obs-websocket/contribute)
Become a financial contributor and help us sustain our community. [Contribute](https://opencollective.com/obs-websocket-dev/contribute)
#### Individuals
<a href="https://opencollective.com/obs-websocket"><img src="https://opencollective.com/obs-websocket/individuals.svg?width=890"></a>
<a href="https://opencollective.com/obs-websocket-dev"><img src="https://opencollective.com/obs-websocket-dev/individuals.svg?width=890"></a>
#### Organizations
Support this project with your organization. Your logo will show up here with a link to your website. [Contribute](https://opencollective.com/obs-websocket/contribute)
Support this project with your organization. Your logo will show up here with a link to your website. [Contribute](https://opencollective.com/obs-websocket-dev/contribute)
<a href="https://opencollective.com/obs-websocket/organization/0/website"><img src="https://opencollective.com/obs-websocket/organization/0/avatar.svg"></a>
<a href="https://opencollective.com/obs-websocket/organization/1/website"><img src="https://opencollective.com/obs-websocket/organization/1/avatar.svg"></a>
<a href="https://opencollective.com/obs-websocket/organization/2/website"><img src="https://opencollective.com/obs-websocket/organization/2/avatar.svg"></a>
<a href="https://opencollective.com/obs-websocket/organization/3/website"><img src="https://opencollective.com/obs-websocket/organization/3/avatar.svg"></a>
<a href="https://opencollective.com/obs-websocket/organization/4/website"><img src="https://opencollective.com/obs-websocket/organization/4/avatar.svg"></a>
<a href="https://opencollective.com/obs-websocket/organization/5/website"><img src="https://opencollective.com/obs-websocket/organization/5/avatar.svg"></a>
<a href="https://opencollective.com/obs-websocket/organization/6/website"><img src="https://opencollective.com/obs-websocket/organization/6/avatar.svg"></a>
<a href="https://opencollective.com/obs-websocket/organization/7/website"><img src="https://opencollective.com/obs-websocket/organization/7/avatar.svg"></a>
<a href="https://opencollective.com/obs-websocket/organization/8/website"><img src="https://opencollective.com/obs-websocket/organization/8/avatar.svg"></a>
<a href="https://opencollective.com/obs-websocket/organization/9/website"><img src="https://opencollective.com/obs-websocket/organization/9/avatar.svg"></a>
<a href="https://opencollective.com/obs-websocket-dev/organization/0/website"><img src="https://opencollective.com/obs-websocket-dev/organization/0/avatar.svg"></a>
<a href="https://opencollective.com/obs-websocket-dev/organization/1/website"><img src="https://opencollective.com/obs-websocket-dev/organization/1/avatar.svg"></a>
<a href="https://opencollective.com/obs-websocket-dev/organization/2/website"><img src="https://opencollective.com/obs-websocket-dev/organization/2/avatar.svg"></a>
<a href="https://opencollective.com/obs-websocket-dev/organization/3/website"><img src="https://opencollective.com/obs-websocket-dev/organization/3/avatar.svg"></a>
<a href="https://opencollective.com/obs-websocket-dev/organization/4/website"><img src="https://opencollective.com/obs-websocket-dev/organization/4/avatar.svg"></a>
<a href="https://opencollective.com/obs-websocket-dev/organization/5/website"><img src="https://opencollective.com/obs-websocket-dev/organization/5/avatar.svg"></a>
<a href="https://opencollective.com/obs-websocket-dev/organization/6/website"><img src="https://opencollective.com/obs-websocket-dev/organization/6/avatar.svg"></a>
<a href="https://opencollective.com/obs-websocket-dev/organization/7/website"><img src="https://opencollective.com/obs-websocket-dev/organization/7/avatar.svg"></a>
<a href="https://opencollective.com/obs-websocket-dev/organization/8/website"><img src="https://opencollective.com/obs-websocket-dev/organization/8/avatar.svg"></a>
<a href="https://opencollective.com/obs-websocket-dev/organization/9/website"><img src="https://opencollective.com/obs-websocket-dev/organization/9/avatar.svg"></a>

View File

@ -1,45 +0,0 @@
# Connecting over a TLS/secure connection (or remotely)
If you want to expose the WebSocket server of obs-websocket over a secure TLS connection (or to connect remotely), the easiest approach is to use a localhost tunneling service like [ngrok](https://ngrok.com/) or [pagekite](https://pagekite.net/).
**Before doing this, secure the WebSocket server first by enabling authentication with a strong password!**
**Please bear in mind that doing this will expose your OBS instance to the open Internet and the security risks it implies. *You've been warned!***
## ngrok
[Install the ngrok CLI tool](https://ngrok.com/download) on a linux OS, then start ngrok bound to port 4444 like this:
```bash
ngrok http 4444
```
The ngrok command will output something like this:
```text
ngrok by @inconshreveable
Tunnel Status online
Version 2.0/2.0
Web Interface http://127.0.0.1:4040
Forwarding http://TUNNEL_ID.ngrok.io -> localhost:4444
Forwarding https://TUNNEL_ID.ngrok.io -> localhost:4444
```
Where `TUNNEL_ID` is, as the name implies, the unique name of your ngrok tunnel. You'll get a new one every time you start ngrok.
Then, use `wss://TUNNEL_ID.ngrok.io` to connect to obs-websocket over TLS.
See the [ngrok documentation](https://ngrok.com/docs) for more tunneling options and settings.
## PageKite
[Install the PageKite CLI tool](http://pagekite.net/downloads), then start PageKite bound to port 4444 like this (replace NAME with one of your choosing):
```bash
python pagekite.py 4444 NAME.pagekite.me
```
Then, use `wss://NAME.pagekite.me` to connect to obs-websocket over TLS.

View File

@ -1,3 +0,0 @@
files:
- source: /data/locale/en-US.ini
translation: /data/locale/%locale%.ini

View File

@ -1,3 +1,5 @@
OBSWebSocket.Plugin.Description="Remote-control of OBS Studio through WebSocket"
OBSWebSocket.Settings.DialogTitle="obs-websocket Settings"
OBSWebSocket.Settings.PluginSettingsTitle="Plugin Settings"
@ -18,6 +20,8 @@ OBSWebSocket.Settings.ShowConnectInfoWarningInfoText="Are you sure that you want
OBSWebSocket.Settings.Save.UserPasswordWarningTitle="Warning: Potential Security Issue"
OBSWebSocket.Settings.Save.UserPasswordWarningMessage="obs-websocket stores the server password as plain text. Using a password generated by obs-websocket is highly recommended."
OBSWebSocket.Settings.Save.UserPasswordWarningInfoText="Are you sure you want to use your own password?"
OBSWebSocket.Settings.Save.PasswordInvalidErrorTitle="Error: Invalid Configuration"
OBSWebSocket.Settings.Save.PasswordInvalidErrorMessage="You must use a password that is 6 or more characters."
OBSWebSocket.SessionTable.Title="Connected WebSocket Sessions"
OBSWebSocket.SessionTable.RemoteAddressColumnTitle="Remote Address"

2
deps/asio vendored

Submodule deps/asio updated: b84e6c16b2...b73dc1d2c0

View File

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

5
docs/.gitignore vendored
View File

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

View File

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

9
docs/build_docs.sh Executable file
View File

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

View File

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

@ -0,0 +1,379 @@
# Main Table of Contents
- [obs-websocket 5.0.0 Protocol](#obs-websocket-500-protocol)
- [Connecting to obs-websocket](#connecting-to-obs-websocket)
- [Connection steps](#connection-steps)
- [Creating an authentication string](#creating-an-authentication-string)
- [Base message types](#message-types)
- [OpCode 0 Hello](#hello-opcode-0)
- [OpCode 1 Identify](#identify-opcode-1)
- [OpCode 2 Identified](#identified-opcode-2)
- [OpCode 3 Reidentify](#reidentify-opcode-3)
- [OpCode 5 Event](#event-opcode-5)
- [OpCode 6 Request](#request-opcode-6)
- [OpCode 7 RequestResponse](#requestresponse-opcode-7)
- [OpCode 8 RequestBatch](#requestbatch-opcode-8)
- [OpCode 9 RequestBatchResponse](#requestbatchresponse-opcode-9)
- [Enums](#enums)
- [Events](#events)
- [Requests](#requests)
# obs-websocket 5.0.0 Protocol
## General Intro
obs-websocket provides a feature-rich RPC communication protocol, giving access to much of OBS's feature set. This document contains everything you should know in order to make a connection and use obs-websocket's functionality to the fullest.
### Design Goals
- Abstraction of identification, events, requests, and batch requests into dedicated message types
- Conformity of request naming using similar terms like `Get`, `Set`, `Get[x]List`, `Start[x]`, `Toggle[x]`
- Conformity of OBS data field names like `sourceName`, `sourceKind`, `sourceType`, `sceneName`, `sceneItemName`
- Error code response system - integer corrosponds to type of error, with optional comment
- Possible support for multiple message encoding options: JSON and MessagePack
- PubSub system - Allow clients to specify which events they do or don't want to receive from OBS
- RPC versioning - Client and server negotiate the latest version of the obs-websocket protocol to communicate with.
## Connecting to obs-websocket
Here's info on how to connect to obs-websocket
---
### Connection steps
These steps should be followed precisely. Failure to connect to the server as instructed will likely result in your client being treated in an undefined way.
- Initial HTTP request made to the obs-websocket server.
- The `Sec-WebSocket-Protocol` header can be used to tell obs-websocket which kind of message encoding to use. By default, obs-websocket uses JSON over text. Available subprotocols:
- `obswebsocket.json` - JSON over text frames
- `obswebsocket.msgpack` - MsgPack over binary frames
- Once the connection is upgraded, the websocket server will immediately send an [OpCode 0 `Hello`](#hello-opcode-0) message to the client.
- The client listens for the `Hello` and responds with an [OpCode 1 `Identify`](#identify-opcode-1) containing all appropriate session parameters.
- If there is an `authentication` field in the `messageData` object, the server requires authentication, and the steps in [Creating an authentication string](#creating-an-authentication-string) should be followed.
- If there is no `authentication` field, the resulting `Identify` object sent to the server does not require an `authentication` string.
- The client determines if the server's `rpcVersion` is supported, and if not it provides its closest supported version in `Identify`.
- The server receives and processes the `Identify` sent by the client.
- If authentication is required and the `Identify` message data does not contain an `authentication` string, or the string is not correct, the connection is closed with `WebSocketCloseCode::AuthenticationFailed`
- If the client has requested an `rpcVersion` which the server cannot use, the connection is closed with `WebSocketCloseCode::UnsupportedRpcVersion`. This system allows both the server and client to have seamless backwards compatability.
- If any other parameters are malformed (invalid type, etc), the connection is closed with an appropriate close code.
- Once identification is processed on the server, the server responds to the client with an [OpCode 2 `Identified`](#identified-opcode-2).
- The client will begin receiving events from obs-websocket and may now make requests to obs-websocket.
- At any time after a client has been identified, it may send an [OpCode 3 `Reidentify`](#reidentify-opcode-3) message to update certain allowed session parameters. The server will respond in the same way it does during initial identification.
#### Connection Notes
- If a binary frame is received when using the `obswebsocket.json` (default) subprotocol, or a text frame is received while using the `obswebsocket.msgpack` subprotocol, the connection is closed with `WebSocketCloseCode::MessageDecodeError`.
- The obs-websocket server listens for any messages containing a `request-type` field in the first level JSON from unidentified clients. If a message matches, the connection is closed with `WebSocketCloseCode::UnsupportedRpcVersion` and a warning is logged.
- If a message with a `messageType` is not recognized to the obs-websocket server, the connection is closed with `WebSocketCloseCode::UnknownOpCode`.
- At no point may the client send any message other than a single `Identify` before it has received an `Identified`. Doing so will result in the connection being closed with `WebSocketCloseCode::NotIdentified`.
---
### Creating an authentication string
obs-websocket uses SHA256 to transmit authentication credentials. The server starts by sending an object in the `authentication` field of its `Hello` message data. The client processes the authentication challenge and responds via the `authentication` string in the `Identify` message data.
For this guide, we'll be using `supersecretpassword` as the password.
The `authentication` object in `Hello` looks like this (example):
```json
{
"challenge": "+IxH4CnCiqpX1rM9scsNynZzbOe4KhDeYcTNS3PDaeY=",
"salt": "lM1GncleQOaCu9lT1yeUZhFYnqhsLLP1G5lAGo3ixaI="
}
```
To generate the authentication string, follow these steps:
- Concatenate the websocket password with the `salt` provided by the server (`password + salt`)
- Generate an SHA256 binary hash of the result and base64 encode it, known as a base64 secret.
- Concatenate the base64 secret with the `challenge` sent by the server (`base64_secret + challenge`)
- Generate a binary SHA256 hash of that result and base64 encode it. You now have your `authentication` string.
For real-world examples of the `authentication` string creation, refer to the obs-websocket client libraries listed on the [README](README.md).
## Message Types (OpCodes)
The following message types are the low-level message types which may be sent to and from obs-websocket.
Messages sent from the obs-websocket server or client may contain these first-level fields, known as the base object:
```
{
"op": number,
"d": object
}
```
- `op` is a `WebSocketOpCode` OpCode.
- `d` is an object of the data fields associated with the operation.
---
### Hello (OpCode 0)
- Sent from: obs-websocket
- Sent to: Freshly connected websocket client
- Description: First message sent from the server immediately on client connection. Contains authentication information if auth is required. Also contains RPC version for version negotiation.
**Data Keys:**
```
{
"obsWebSocketVersion": string,
"rpcVersion": number,
"authentication": object(optional)
}
```
- `rpcVersion` is a version number which gets incremented on each **breaking change** to the obs-websocket protocol. Its usage in this context is to provide the current rpc version that the server would like to use.
**Example Messages:**
Authentication is required
```json
{
"op": 0,
"d": {
"obsWebSocketVersion": "5.0.0",
"rpcVersion": 1,
"authentication": {
"challenge": "+IxH4CnCiqpX1rM9scsNynZzbOe4KhDeYcTNS3PDaeY=",
"salt": "lM1GncleQOaCu9lT1yeUZhFYnqhsLLP1G5lAGo3ixaI="
}
}
}
```
Authentication is not required
```json
{
"op": 0,
"d": {
"obsWebSocketVersion": "5.0.0",
"rpcVersion": 1
}
}
```
---
### Identify (OpCode 1)
- Sent from: Freshly connected websocket client
- Sent to: obs-websocket
- Description: Response to `Hello` message, should contain authentication string if authentication is required, along with PubSub subscriptions and other session parameters.
**Data Keys:**
```
{
"rpcVersion": number,
"authentication": string(optional),
"eventSubscriptions": number(optional) = (EventSubscription::All)
}
```
- `rpcVersion` is the version number that the client would like the obs-websocket server to use.
- `eventSubscriptions` is a bitmask of `EventSubscriptions` items to subscribe to events and event categories at will. By default, all event categories are subscribed, except for events marked as high volume. High volume events must be explicitly subscribed to.
**Example Message:**
```json
{
"op": 1,
"d": {
"rpcVersion": 1,
"authentication": "Dj6cLS+jrNA0HpCArRg0Z/Fc+YHdt2FQfAvgD1mip6Y=",
"eventSubscriptions": 33
}
}
```
---
### Identified (OpCode 2)
- Sent from: obs-websocket
- Sent to: Freshly identified client
- Description: The identify request was received and validated, and the connection is now ready for normal operation.
**Data Keys:**
```
{
"negotiatedRpcVersion": number
}
```
- If rpc version negotiation succeeds, the server determines the RPC version to be used and gives it to the client as `negotiatedRpcVersion`
**Example Message:**
```json
{
"op": 2,
"d": {
"negotiatedRpcVersion": 1
}
}
```
---
### Reidentify (OpCode 3)
- Sent from: Identified client
- Sent to: obs-websocket
- Description: Sent at any time after initial identification to update the provided session parameters.
**Data Keys:**
```
{
"eventSubscriptions": number(optional) = (EventSubscription::All)
}
```
- Only the listed parameters may be changed after initial identification. To change a parameter not listed, you must reconnect to the obs-websocket server.
---
### Event (OpCode 5)
- Sent from: obs-websocket
- Sent to: All subscribed and identified clients
- Description: An event coming from OBS has occured. Eg scene switched, source muted.
**Data Keys:**
```
{
"eventType": string,
"eventIntent": number,
"eventData": object(optional)
}
```
- `eventIntent` is the original intent required to be subscribed to in order to receive the event.
**Example Message:**
```json
{
"op": 5,
"d": {
"eventType": "StudioModeStateChanged",
"eventIntent": 1,
"eventData": {
"studioModeEnabled": true
}
}
}
```
---
### Request (OpCode 6)
- Sent from: Identified client
- Sent to: obs-websocket
- Description: Client is making a request to obs-websocket. Eg get current scene, create source.
**Data Keys:**
```
{
"requestType": string,
"requestId": string,
"requestData": object(optional),
}
```
**Example Message:**
```json
{
"op": 6,
"d": {
"requestType": "SetCurrentScene",
"requestId": "f819dcf0-89cc-11eb-8f0e-382c4ac93b9c",
"requestData": {
"sceneName": "Scene 12"
}
}
}
```
---
### RequestResponse (OpCode 7)
- Sent from: obs-websocket
- Sent to: Identified client which made the request
- Description: obs-websocket is responding to a request coming from a client.
**Data Keys:**
```
{
"requestType": string,
"requestId": string,
"requestStatus": object,
"responseData": object(optional)
}
```
- The `requestType` and `requestId` are simply mirrors of what was sent by the client.
`requestStatus` object:
```
{
"result": bool,
"code": number,
"comment": string(optional)
}
```
- `result` is `true` if the request resulted in `RequestStatus::Success`. False if otherwise.
- `code` is a `RequestStatus` code.
- `comment` may be provided by the server on errors to offer further details on why a request failed.
**Example Messages:**
Successful Response
```json
{
"op": 7,
"d": {
"requestType": "SetCurrentScene",
"requestId": "f819dcf0-89cc-11eb-8f0e-382c4ac93b9c",
"requestStatus": {
"result": true,
"code": 100
}
}
}
```
Failure Response
```json
{
"op": 7,
"d": {
"requestType": "SetCurrentScene",
"requestId": "f819dcf0-89cc-11eb-8f0e-382c4ac93b9c",
"requestStatus": {
"result": false,
"code": 608,
"comment": "Parameter: sceneName"
}
}
}
```
---
### RequestBatch (OpCode 8)
- Sent from: Identified client
- Sent to: obs-websocket
- Description: Client is making a batch of requests for obs-websocket. Requests are processed serially (in order) by the server.
**Data Keys:**
```
{
"requestId": string,
"haltOnFailure": bool(optional) = false,
"executionType": number(optional) = RequestBatchExecutionType::SerialRealtime
"requests": array<object>
}
```
- When `haltOnFailure` is `true`, the processing of requests will be halted on first failure. Returns only the processed requests in [`RequestBatchResponse`](#requestbatchresponse-opcode-9).
- Requests in the `requests` array follow the same structure as the `Request` payload data format, however `requestId` is an optional field.
---
### RequestBatchResponse (OpCode 9)
- Sent from: obs-websocket
- Sent to: Identified client which made the request
- Description: obs-websocket is responding to a request batch coming from the client.
**Data Keys:**
```
{
"requestId": string,
"results": array<object>
}
```

View File

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

View File

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

View File

@ -1 +0,0 @@
{}

5308
docs/generated/protocol.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -1,604 +0,0 @@
# obs-websocket 5.0.0 protocol reference
## General Introduction
obs-websocket provides a feature-rich RPC communication protocol, giving access to much of OBS's feature set. This document contains everything you should know in order to make a connection and use obs-websocket's functionality to the fullest.
### Design Goals
- Abstraction of identification, events, requests, and batch requests into dedicated message types
- Conformity of request naming using similar terms like `Get`, `Set`, `Get[x]List`, `Start[x]`, `Toggle[x]`
- Conformity of OBS data key names like `sourceName`, `sourceKind`, `sourceType`, `sceneName`, `sceneItemName`
- Error code response system - integer corrosponds to type of error, with optional comment
- Possible support for multiple message encoding options: JSON and MessagePack
- PubSub system - Allow clients to specify which events they do or don't want to receive from OBS
- RPC versioning - Client and server negotiate the latest version of the obs-websocket protocol to communicate with.
## Table of Contents
- [Connecting to obs-websocket](#connecting-to-obs-websocket)
- [Connection steps](#connection-steps)
- [Creating an authentication string](#creating-an-authentication-string)
- [Enumerations](#enumerations)
- [Base message types](#message-types)
- [OpCode 0 Hello](#hello-opcode-0)
- [OpCode 1 Identify](#identify-opcode-1)
- [OpCode 2 Identified](#identified-opcode-2)
- [OpCode 3 Reidentify](#reidentify-opcode-3)
- [OpCode 5 Event](#event-opcode-5)
- [OpCode 6 Request](#request-opcode-6)
- [OpCode 7 RequestResponse](#requestresponse-opcode-7)
- [OpCode 8 RequestBatch](#requestbatch-opcode-8)
- [OpCode 9 RequestBatchResponse](#requestbatchresponse-opcode-9)
- [Events](#events)
- [Requests](#requests)
## Connecting to obs-websocket
Here's info on how to connect to obs-websocket
---
### Connection steps
These steps should be followed precisely. Failure to connect to the server as instructed will likely result in your client being treated in an undefined way.
- Initial HTTP request made to the obs-websocket server.
- HTTP request headers can be used to set the websocket communication type. The default format is JSON. Example headers:
- `Content-Type: application/json`
- `Content-Type: application/msgpack`
- If an invalid `Content-Type` is specified, the connection will be closed with [`WebSocketCloseCode::InvalidContentType`](#websocketclosecode-enum) after upgrade (but before `Hello`).
- Once the connection is upgraded, the websocket server will immediately send an [OpCode 0 `Hello`](#hello-opcode-0) message to the client.
- The client listens for the `Hello` and responds with an [OpCode 1 `Identify`](#identify-opcode-1) containing all appropriate session parameters.
- If there is an `authentication` key in the `messageData` object, the server requires authentication, and the steps in [Creating an authentication string](#creating-an-authentication-string) should be followed.
- If there is no `authentication` key, the resulting `Identify` object sent to the server does not require an `authentication` string.
- The client determines if the server's `rpcVersion` is supported, and if not it provides its closest supported version in `Identify`.
- The server receives and processes the `Identify` sent by the client.
- If authentication is required and the `Identify` message data does not contain an `authentication` string, or the string is not correct, the connection is dropped with [`WebSocketCloseCode::AuthenticationFailed`](#websocketclosecode-enum)
- If the client has requested an `rpcVersion` which the server cannot use, the connection is dropped with [`WebSocketCloseCode::UnsupportedProtocolVersion`](#websocketclosecode-enum). This system allows both the server and client to have seamless backwards compatability.
- If any other parameters are malformed (invalid type, etc), the connection is dropped with [`WebSocketCloseCode::InvalidIdentifyParameter`](#websocketclosecode-enum)
- Once identification is processed on the server, the server responds to the client with an [OpCode 2 `Identified`](#identified-opcode-2).
- The client will begin receiving events from obs-websocket and may now make requests to obs-websocket.
- At any time after a client has been identified, it may send an [OpCode 3 `Reidentify`](#reidentify-opcode-3) message to update certain allowed session parameters. The server will respond in the same way it does during initial identification.
#### Connection Notes
- If the Content Type is `application/msgpack`, all messages must be sent over binary. If it is `application/json`, all messages must be sent over text.
- The obs-websocket server listens for any messages containing a `request-type` key in the first level JSON from unidentified clients. If a message matches, the connection is dropped with [`WebSocketCloseCode::UnsupportedProtocolVersion`](#websocketclosecode-enum) and a warning is logged.
- If a message with a `messageType` is not recognized to the obs-websocket server, the connection is dropped with [`WebSocketCloseCode::UnknownMessageType`](#websocketclosecode-enum).
- At no point may the client send any message other than a single `Identify` before it has received an `Identified`. Doing so will result in the connection being dropped by the server with [`WebSocketCloseCode::NotIdentified`](#websocketclosecode-enum).
---
### Creating an authentication string
obs-websocket uses SHA256 to transmit authentication credentials. The server starts by sending an object in the `authentication` key of its `Hello` message data. The client processes the authentication challenge and responds via the `authentication` string in the `Identify` message data.
For this guide, we'll be using `supersecretpassword` as the password.
The `authentication` object in `Hello` looks like this (example):
```json
{
"challenge": "+IxH4CnCiqpX1rM9scsNynZzbOe4KhDeYcTNS3PDaeY=",
"salt": "lM1GncleQOaCu9lT1yeUZhFYnqhsLLP1G5lAGo3ixaI="
}
```
To generate the authentication string, follow these steps:
- Concatenate the websocket password with the `salt` provided by the server (`password + salt`)
- Generate an SHA256 binary hash of the result and base64 encode it, known as a base64 secret.
- Concatenate the base64 secret with the `challenge` sent by the server (`base64_secret + challenge`)
- Generate a binary SHA256 hash of that result and base64 encode it. You now have your `authentication` string.
For real-world examples of the `authentication` string creation, refer to the obs-websocket client libraries listed on the [README](README.md).
---
### Enumerations
These are the enumeration definitions for various codes used by obs-websocket.
#### WebSocketOpCode Enum
```cpp
enum WebSocketOpCode {
Hello = 0,
Identify = 1,
Identified = 2,
Reidentify = 3,
Event = 5,
Request = 6,
RequestResponse = 7,
RequestBatch = 8,
RequestBatchResponse = 9,
};
```
#### WebSocketCloseCode Enum
```cpp
enum WebSocketCloseCode {
// Internal only
DontClose = 0,
// Reserved
UnknownReason = 4000,
// The requested `Content-Type` specified in the request HTTP header is invalid.
InvalidContentType = 4001,
// The server was unable to decode the incoming websocket message
MessageDecodeError = 4002,
// A data key is missing but required
MissingDataKey = 4003,
// A data key has an invalid type
InvalidDataKeyType = 4004,
// The specified `op` was invalid or missing
UnknownOpCode = 4005,
// The client sent a websocket message without first sending `Identify` message
NotIdentified = 4006,
// The client sent an `Identify` message while already identified
AlreadyIdentified = 4007,
// The authentication attempt (via `Identify`) failed
AuthenticationFailed = 4008,
// The server detected the usage of an old version of the obs-websocket protocol.
UnsupportedRpcVersion = 4009,
// The websocket session has been invalidated by the obs-websocket server.
SessionInvalidated = 4010,
};
```
#### EventSubscriptions Enum
```cpp
enum EventSubscription {
// Set subscriptions to 0 to disable all events
None = 0,
// Receive events in the `General` category
General = (1 << 0),
// Receive events in the `Config` category
Config = (1 << 1),
// Receive events in the `Scenes` category
Scenes = (1 << 2),
// Receive events in the `Inputs` category
Inputs = (1 << 3),
// Receive events in the `Transitions` category
Transitions = (1 << 4),
// Receive events in the `Filters` category
Filters = (1 << 5),
// Receive events in the `Outputs` category
Outputs = (1 << 6),
// Receive events in the `Scene Items` category
SceneItems = (1 << 7),
// Receive events in the `MediaInputs` category
MediaInputs = (1 << 8),
// Receive all event categories
All = (General | Config | Scenes | Inputs | Transitions | Filters | Outputs | SceneItems | MediaInputs),
// InputVolumeMeters event (high-volume)
InputVolumeMeters = (1 << 9),
// InputActiveStateChanged event (high-volume)
InputActiveStateChanged = (1 << 10),
// InputShowStateChanged event (high-volume)
InputShowStateChanged = (1 << 11),
};
```
Subscriptions are a bitmask system. In many languages, to generate a bitmask that subscribes to `General` and `Scenes`, you would do: `subscriptions = ((1 << 0) | (1 << 2))`
#### RequestStatus Enum
```cpp
enum RequestStatus {
Unknown = 0,
// For internal use to signify a successful parameter check
NoError = 10,
Success = 100,
// The `requestType` key is missing from the request data
MissingRequestType = 203,
// The request type is invalid (does not exist)
UnknownRequestType = 204,
// Generic error code (comment is expected to be provided)
GenericError = 205,
// A required request parameter is missing
MissingRequestParameter = 300,
// The request does not have a valid requestData object.
MissingRequestData = 301,
// Generic invalid request parameter message
InvalidRequestParameter = 400,
// A request parameter has the wrong data type
InvalidRequestParameterDataType = 401,
// A request parameter (float or int) is out of valid range
RequestParameterOutOfRange = 402,
// A request parameter (string or array) is empty and cannot be
RequestParameterEmpty = 403,
// There are too many request parameters (eg. a request takes two optionals, where only one is allowed at a time)
TooManyRequestParameters = 404,
// An output is running and cannot be in order to perform the request (generic)
OutputRunning = 500,
// An output is not running and should be
OutputNotRunning = 501,
// Stream is running and cannot be
StreamRunning = 502,
// Stream is not running and should be
StreamNotRunning = 503,
// Record is running and cannot be
RecordRunning = 504,
// Record is not running and should be
RecordNotRunning = 505,
// Record is paused and cannot be
RecordPaused = 506,
// Replay buffer is running and cannot be
ReplayBufferRunning = 507,
// Replay buffer is not running and should be
ReplayBufferNotRunning = 508,
// Replay buffer is disabled and cannot be
ReplayBufferDisabled = 509,
// Studio mode is active and cannot be
StudioModeActive = 510,
// Studio mode is not active and should be
StudioModeNotActive = 511,
// Virtualcam is running and cannot be
VirtualcamRunning = 512,
// Virtualcam is not running and should be
VirtualcamNotRunning = 513,
// The specified source (obs_source_t) was of the invalid type (Eg. input instead of scene)
InvalidSourceType = 600,
// The specified source (obs_source_t) was not found (generic for input, filter, transition, scene)
SourceNotFound = 601,
// The specified source (obs_source_t) already exists. Applicable to inputs, filters, transitions, scenes
SourceAlreadyExists = 602,
// The specified input (obs_source_t-OBS_SOURCE_TYPE_FILTER) was not found
InputNotFound = 603,
// The specified input (obs_source_t-OBS_SOURCE_TYPE_INPUT) had the wrong kind
InvalidInputKind = 604,
// The specified filter (obs_source_t-OBS_SOURCE_TYPE_FILTER) was not found
FilterNotFound = 605,
// The specified transition (obs_source_t-OBS_SOURCE_TYPE_TRANSITION) was not found
TransitionNotFound = 606,
// The specified transition (obs_source_t-OBS_SOURCE_TYPE_TRANSITION) does not support setting its position (transition is of fixed type)
TransitionDurationFixed = 607,
// The specified scene (obs_source_t-OBS_SOURCE_TYPE_SCENE), (obs_scene_t) was not found
SceneNotFound = 608,
// The specified scene item (obs_sceneitem_t) was not found
SceneItemNotFound = 609,
// The specified scene collection was not found
SceneCollectionNotFound = 610,
// The specified profile was not found
ProfileNotFound = 611,
// The specified output (obs_output_t) was not found
OutputNotFound = 612,
// The specified encoder (obs_encoder_t) was not found
EncoderNotFound = 613,
// The specified service (obs_service_t) was not found
ServiceNotFound = 614,
// The specified hotkey was not found
HotkeyNotFound = 615,
// The specified directory was not found
DirectoryNotFound = 616,
// The specified config item (config_t) was not found. Could be section or parameter name
ConfigParameterNotFound = 617,
// The specified property (obs_properties_t) was not found
PropertyNotFound = 618,
// The specififed key (OBS_KEY_*) was not found
KeyNotFound = 619,
// The specified data realm (OBS_WEBSOCKET_DATA_REALM_*) was not found
DataRealmNotFound = 620,
// The scene collection already exists
SceneCollectionAlreadyExists = 621,
// There are not enough scene collections to perform the action
NotEnoughSceneCollections = 622,
// The profile already exists
ProfileAlreadyExists = 623,
// There are not enough profiles to perform the action
NotEnoughProfiles = 624,
// There are not enough scenes to perform the action
NotEnoughScenes = 625,
// Processing the request failed unexpectedly
RequestProcessingFailed = 700,
// Starting the Output failed
OutputStartFailed = 701,
// Duplicating the scene item failed
SceneItemDuplicationFailed = 702,
// Rendering the screenshot failed
ScreenshotRenderFailed = 703,
// Encoding the screenshot failed
ScreenshotEncodeFailed = 704,
// Saving the screenshot failed
ScreenshotSaveFailed = 705,
// Creating the directory failed
DirectoryCreationFailed = 706,
// The combination of request parameters cannot be used to perform an action
CannotAct = 707,
// Creation of a new stream service failed
StreamServiceCreationFailed = 708,
};
```
## Message Types (OpCodes)
The following message types are the low-level message types which may be sent to and from obs-websocket.
Messages sent from the obs-websocket server or client may contain these first-level keys, known as the base object:
```
{
"op": number,
"d": object
}
```
- `op` is a [`WebSocketOpCode` OpCode.](#websocketopcode-enum)
- `d` is an object of the data keys associated with the operation.
---
### Hello (OpCode 0)
- Sent from: obs-websocket
- Sent to: Freshly connected websocket client
- Description: First message sent from the server immediately on client connection. Contains authentication information if auth is required. Also contains RPC version for version negotiation.
**Data Keys:**
```
{
"obsWebSocketVersion": string,
"rpcVersion": number,
"authentication": object(optional)
}
```
- `rpcVersion` is a version number which gets incremented on each **breaking change** to the obs-websocket protocol. Its usage in this context is to provide the current rpc version that the server would like to use.
**Example Messages:**
Authentication is required
```json
{
"op": 0,
"d": {
"obsWebSocketVersion": "5.0.0",
"rpcVersion": 1,
"authentication": {
"challenge": "+IxH4CnCiqpX1rM9scsNynZzbOe4KhDeYcTNS3PDaeY=",
"salt": "lM1GncleQOaCu9lT1yeUZhFYnqhsLLP1G5lAGo3ixaI="
}
}
}
```
Authentication is not required
```json
{
"op": 0,
"d": {
"obsWebSocketVersion": "5.0.0",
"rpcVersion": 1
}
}
```
---
### Identify (OpCode 1)
- Sent from: Freshly connected websocket client
- Sent to: obs-websocket
- Description: Response to `Hello` message, should contain authentication string if authentication is required, along with PubSub subscriptions and other session parameters.
**Data Keys:**
```
{
"rpcVersion": number,
"authentication": string(optional),
"ignoreInvalidMessages": bool(optional) = false,
"ignoreNonFatalRequestChecks": bool(optional) = false,
"eventSubscriptions": number(optional) = (EventSubscription::All)
}
```
- `rpcVersion` is the version number that the client would like the obs-websocket server to use.
- When `ignoreInvalidMessages` is true, the socket will not be closed for [`WebSocketCloseCode`](#websocketclosecode-enum): `MessageDecodeError`, `UnknownMessageType`, or `RequestMissingRequestId`. Instead, the message will be logged and dropped.
- When `ignoreNonFatalRequestChecks` is true, requests will ignore checks which are not critical to the function of the request. Eg calling `DeleteScene` when the target scene does not exist would still return [`RequestStatus::Success`](#requeststatus-enum) if this flag is enabled.
- `eventSubscriptions` is a bitmask of [`EventSubscriptions`](#eventsubscriptions-enum) items to subscribe to events and event categories at will. By default, all event categories are subscribed, except for events marked as high volume. High volume events must be explicitly subscribed to.
**Example Message:**
```json
{
"op": 1,
"d": {
"rpcVersion": 1,
"authentication": "Dj6cLS+jrNA0HpCArRg0Z/Fc+YHdt2FQfAvgD1mip6Y=",
"eventSubscriptions": 33
}
}
```
---
### Identified (OpCode 2)
- Sent from: obs-websocket
- Sent to: Freshly identified client
- Description: The identify request was received and validated, and the connection is now ready for normal operation.
**Data Keys:**
```
{
"negotiatedRpcVersion": number
}
```
- If rpc version negotiation succeeds, the server determines the RPC version to be used and gives it to the client as `negotiatedRpcVersion`
**Example Message:**
```json
{
"op": 2,
"d": {
"negotiatedRpcVersion": 1
}
}
```
---
### Reidentify (OpCode 3)
- Sent from: Identified client
- Sent to: obs-websocket
- Description: Sent at any time after initial identification to update the provided session parameters.
**Data Keys:**
```
{
"ignoreInvalidMessages": bool(optional) = false,
"ignoreNonFatalRequestChecks": bool(optional) = false,
"eventSubscriptions": number(optional) = (EventSubscription::All)
}
```
- Only the listed parameters may be changed after initial identification. To change a parameter not listed, you must reconnect to the obs-websocket server.
---
### Event (OpCode 5)
- Sent from: obs-websocket
- Sent to: All subscribed and identified clients
- Description: An event coming from OBS has occured. Eg scene switched, source muted.
**Data Keys:**
```
{
"eventType": string,
"eventData": object(optional)
}
```
**Example Message:**
```json
{
"op": 2,
"d": {
"eventType": "StudioModeStateChanged",
"eventData": {
"studioModeEnabled": true
}
}
}
```
---
### Request (OpCode 6)
- Sent from: Identified client
- Sent to: obs-websocket
- Description: Client is making a request to obs-websocket. Eg get current scene, create source.
**Data Keys:**
```
{
"requestType": string,
"requestId": string,
"requestData": object(optional),
}
```
**Example Message:**
```json
{
"op": 6,
"d": {
"requestType": "SetCurrentScene",
"requestId": "f819dcf0-89cc-11eb-8f0e-382c4ac93b9c",
"requestData": {
"sceneName": "Scene 12"
}
}
}
```
---
### RequestResponse (OpCode 7)
- Sent from: obs-websocket
- Sent to: Identified client which made the request
- Description: obs-websocket is responding to a request coming from a client.
**Data Keys:**
```
{
"requestType": string,
"requestId": string,
"requestStatus": object,
"responseData": object(optional)
}
```
- The `requestType` and `requestId` are simply mirrors of what was sent by the client.
`requestStatus` object:
```
{
"result": bool,
"code": number,
"comment": string(optional)
}
```
- `result` is `true` if the request resulted in [`RequestStatus::Success`](#requeststatus-enum). False if otherwise.
- `code` is a [`RequestStatus`](#requeststatus-enum) code.
- `comment` may be provided by the server on errors to offer further details on why a request failed.
**Example Messages:**
Successful Response
```json
{
"op": 7,
"d": {
"requestType": "SetCurrentScene",
"requestId": "f819dcf0-89cc-11eb-8f0e-382c4ac93b9c",
"requestStatus": {
"result": true,
"code": 100
}
}
}
```
Failure Response
```json
{
"op": 7,
"d": {
"requestType": "SetCurrentScene",
"requestId": "f819dcf0-89cc-11eb-8f0e-382c4ac93b9c",
"requestStatus": {
"result": false,
"code": 608,
"comment": "Parameter: sceneName"
}
}
}
```
---
### RequestBatch (OpCode 8)
- Sent from: Identified client
- Sent to: obs-websocket
- Description: Client is making a batch of requests for obs-websocket. Requests are processed serially (in order) by the server.
**Data Keys:**
```
{
"requestId": string,
"haltOnFailure": bool(optional) = false,
"requests": array<object>
}
```
- When `haltOnFailure` is `true`, the processing of requests will be halted on first failure. Returns only the processed requests in [`RequestBatchResponse`](#requestbatchresponse-opcode-9).
- Requests in the `requests` array follow the same structure as the `Request` payload data format, however `requestId` is an optional key.
---
### RequestBatchResponse (OpCode 9)
- Sent from: obs-websocket
- Sent to: Identified client which made the request
- Description: obs-websocket is responding to a request batch coming from the client.
**Data Keys:**
```
{
"requestId": string,
"results": array<object>
}
```

View File

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

View File

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

5
docs/versions.json Normal file
View File

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

View File

@ -1,63 +0,0 @@
#define MyAppName "obs-websocket"
#define MyAppVersion "5.0.0"
#define MyAppPublisher "Stephane Lepin"
#define MyAppURL "http://github.com/Palakis/obs-websocket"
[Setup]
; NOTE: The value of AppId uniquely identifies this application.
; Do not use the same AppId value in installers for other applications.
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
AppId={{117EE44F-48E1-49E5-A381-CC8D9195CF35}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName={code:GetDirName}
DefaultGroupName={#MyAppName}
OutputBaseFilename=obs-websocket-{#MyAppVersion}-Windows-Installer
Compression=lzma
SolidCompression=yes
DirExistsWarning=no
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Files]
Source: "..\release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "..\LICENSE"; Flags: dontcopy
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[Icons]
Name: "{group}\{cm:ProgramOnTheWeb,{#MyAppName}}"; Filename: "{#MyAppURL}"
Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"
[Code]
procedure InitializeWizard();
var
GPLText: AnsiString;
Page: TOutputMsgMemoWizardPage;
begin
ExtractTemporaryFile('LICENSE');
LoadStringFromFile(ExpandConstant('{tmp}\LICENSE'), GPLText);
Page := CreateOutputMsgMemoPage(wpWelcome,
'License Information', 'Please review the license terms before installing obs-websocket',
'Press Page Down to see the rest of the agreement. Once you are aware of your rights, click Next to continue.',
String(GPLText)
);
end;
// credit where it's due :
// following function come from https://github.com/Xaymar/obs-studio_amf-encoder-plugin/blob/master/%23Resources/Installer.in.iss#L45
function GetDirName(Value: string): string;
var
InstallPath: string;
begin
// initialize default path, which will be returned when the following registry
// key queries fail due to missing keys or for some different reason
Result := '{pf}\obs-studio';
// query the first registry value; if this succeeds, return the obtained value
if RegQueryStringValue(HKLM32, 'SOFTWARE\OBS Studio', '', InstallPath) then
Result := InstallPath
end;

View File

@ -1,7 +1,7 @@
#define MyAppName "obs-websocket"
#define MyAppVersion "@CMAKE_PROJECT_VERSION@"
#define MyAppPublisher "Stephane Lepin"
#define MyAppURL "http://github.com/Palakis/obs-websocket"
#define MyAppVersion "@OBS_WEBSOCKET_VERSION@"
#define MyAppPublisher "obs-websocket"
#define MyAppURL "http://github.com/obsproject/obs-websocket"
[Setup]
; NOTE: The value of AppId uniquely identifies this application.
@ -48,6 +48,17 @@ begin
);
end;
// Validate that obs-studio is installed before installing the plugin
function PrepareToInstall(var NeedsRestart: Boolean): String;
begin
Result := '';
if not DirExists(ExpandConstant('{app}\obs-plugins')) then
begin
Result := 'The selected install directory does not appear to be valid. Please install OBS Studio before installing {#MyAppName} or correct your install path.';
end;
end;
// credit where it's due :
// following function come from https://github.com/Xaymar/obs-studio_amf-encoder-plugin/blob/master/%23Resources/Installer.in.iss#L45
function GetDirName(Value: string): string;
@ -56,7 +67,7 @@ var
begin
// initialize default path, which will be returned when the following registry
// key queries fail due to missing keys or for some different reason
Result := '{pf}\obs-studio';
Result := '{commonpf}\obs-studio';
// query the first registry value; if this succeeds, return the obtained value
if RegQueryStringValue(HKLM32, 'SOFTWARE\OBS Studio', '', InstallPath) then
Result := InstallPath

View File

@ -0,0 +1,40 @@
#include <obs-module.h>
#include "../obs-websocket-api.h"
OBS_DECLARE_MODULE()
OBS_MODULE_USE_DEFAULT_LOCALE(PLUGIN_NAME, "en-US")
obs_websocket_vendor vendor;
bool obs_module_load(void)
{
blog(LOG_INFO, "Example obs-websocket-api plugin loaded!");
return true;
}
void example_request_cb(obs_data_t *request_data, obs_data_t *response_data, void *priv_data);
void obs_module_post_load(void)
{
vendor = obs_websocket_register_vendor("api_example_plugin");
if (!vendor) {
blog(LOG_ERROR, "Vendor registration failed! (obs-websocket should have logged something if installed properly.)");
return;
}
if (!obs_websocket_vendor_register_request(vendor, "example_request", example_request_cb, NULL))
blog(LOG_ERROR, "Failed to register `example_request` request with obs-websocket.");
}
void obs_module_unload(void)
{
blog(LOG_INFO, "Example obs-websocket-api plugin unloaded!");
}
void example_request_cb(obs_data_t *request_data, obs_data_t *response_data, void *priv_data)
{
if (obs_data_has_user_value(request_data, "ping"))
obs_data_set_bool(response_data, "pong", true);
UNUSED_PARAMETER(priv_data);
}

135
lib/obs-websocket-api.h Normal file
View File

@ -0,0 +1,135 @@
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#ifndef _OBS_WEBSOCKET_API_H
#define _OBS_WEBSOCKET_API_H
#include <obs.h>
#define OBS_WEBSOCKET_API_VERSION 1
#ifdef __cplusplus
extern "C" {
#endif
typedef void* obs_websocket_vendor;
typedef void (*obs_websocket_request_callback_function)(obs_data_t*, obs_data_t*, void*);
struct obs_websocket_request_callback {
obs_websocket_request_callback_function callback;
void *priv_data;
};
inline proc_handler_t *ph;
static inline proc_handler_t *obs_websocket_get_ph(void)
{
proc_handler_t *global_ph = obs_get_proc_handler();
assert(global_ph != NULL);
calldata_t cd = {0};
if (!proc_handler_call(global_ph, "obs_websocket_api_get_ph", &cd))
blog(LOG_DEBUG, "Unable to fetch obs-websocket proc handler object. obs-websocket not installed?");
proc_handler_t *ret = (proc_handler_t*)calldata_ptr(&cd, "ph");
calldata_free(&cd);
return ret;
}
static inline bool obs_websocket_run_simple_proc(obs_websocket_vendor vendor, const char *proc_name, calldata_t *cd)
{
if (!ph || !vendor || !proc_name || !strlen(proc_name) || !cd)
return false;
calldata_set_ptr(cd, "vendor", vendor);
proc_handler_call(ph, proc_name, cd);
return calldata_bool(cd, "success");
}
// ALWAYS CALL VIA `obs_module_post_load()` CALLBACK!
// Registers a new "vendor" (Example: obs-ndi)
static inline obs_websocket_vendor obs_websocket_register_vendor(const char *vendor_name)
{
ph = obs_websocket_get_ph();
if (!ph)
return NULL;
calldata_t cd = {0};
calldata_set_string(&cd, "name", vendor_name);
proc_handler_call(ph, "vendor_register", &cd);
obs_websocket_vendor ret = calldata_ptr(&cd, "vendor");
calldata_free(&cd);
return ret;
}
// Registers a new request for a vendor
static inline bool obs_websocket_vendor_register_request(obs_websocket_vendor vendor, const char *request_type, obs_websocket_request_callback_function request_callback, void* priv_data)
{
calldata_t cd = {0};
struct obs_websocket_request_callback cb = {};
cb.callback = request_callback;
cb.priv_data = priv_data;
calldata_set_string(&cd, "type", request_type);
calldata_set_ptr(&cd, "callback", &cb);
bool success = obs_websocket_run_simple_proc(vendor, "vendor_request_register", &cd);
calldata_free(&cd);
return success;
}
// Unregisters an existing vendor request
static inline bool obs_websocket_vendor_unregister_request(obs_websocket_vendor vendor, const char *request_type)
{
calldata_t cd = {0};
calldata_set_string(&cd, "type", request_type);
bool success = obs_websocket_run_simple_proc(vendor, "vendor_request_unregister", &cd);
calldata_free(&cd);
return success;
}
// Does not affect event_data refcount.
// Emits an event under the vendor's name
static inline bool obs_websocket_vendor_emit_event(obs_websocket_vendor vendor, const char *event_name, obs_data_t *event_data)
{
calldata_t cd = {0};
calldata_set_string(&cd, "type", event_name);
calldata_set_ptr(&cd, "data", (void*)event_data);
bool success = obs_websocket_run_simple_proc(vendor, "vendor_event_emit", &cd);
calldata_free(&cd);
return success;
}
#ifdef __cplusplus
}
#endif
#endif

View File

@ -1,7 +1,25 @@
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#include <obs-frontend-api.h>
#include "Config.h"
#include "plugin-macros.generated.h"
#include "utils/Crypto.h"
#include "utils/Platform.h"
@ -11,7 +29,7 @@
#define PARAM_ENABLED "ServerEnabled"
#define PARAM_PORT "ServerPort"
#define PARAM_ALERTS "AlertsEnabled"
#define PARAM_AUTHREQUIRED "AuthRequred"
#define PARAM_AUTHREQUIRED "AuthRequired"
#define PARAM_PASSWORD "ServerPassword"
#define CMDLINE_WEBSOCKET_PORT "websocket_port"
@ -19,11 +37,11 @@
#define CMDLINE_WEBSOCKET_DEBUG "websocket_debug"
Config::Config() :
FirstLoad(true),
PortOverridden(false),
PasswordOverridden(false),
FirstLoad(true),
ServerEnabled(true),
ServerPort(4444),
ServerPort(4455),
DebugEnabled(false),
AlertsEnabled(false),
AuthRequired(true),
@ -43,39 +61,53 @@ void Config::Load()
FirstLoad = config_get_bool(obsConfig, CONFIG_SECTION_NAME, PARAM_FIRSTLOAD);
ServerEnabled = config_get_bool(obsConfig, CONFIG_SECTION_NAME, PARAM_ENABLED);
AlertsEnabled = config_get_bool(obsConfig, CONFIG_SECTION_NAME, PARAM_ALERTS);
ServerPort = config_get_uint(obsConfig, CONFIG_SECTION_NAME, PARAM_PORT);
AuthRequired = config_get_bool(obsConfig, CONFIG_SECTION_NAME, PARAM_AUTHREQUIRED);
ServerPassword = config_get_string(obsConfig, CONFIG_SECTION_NAME, PARAM_PASSWORD);
// Set server password and save it to the config before processing overrides,
// so that there is always a true configured password regardless of if
// future loads use the override flag.
if (FirstLoad) {
FirstLoad = false;
if (ServerPassword.isEmpty()) {
blog(LOG_INFO, "[Config::Load] (FirstLoad) Generating new server password.");
ServerPassword = QString::fromStdString(Utils::Crypto::GeneratePassword());
} else {
blog(LOG_INFO, "[Config::Load] (FirstLoad) Not generating new password since one is already configured.");
}
Save();
}
// Process `--websocket_port` override
QString portArgument = Utils::Platform::GetCommandLineArgument(CMDLINE_WEBSOCKET_PORT);
if (portArgument != "") {
bool ok;
uint16_t serverPort = portArgument.toUShort(&ok);
if (ok) {
blog(LOG_INFO, "[Config::Load] Overriding websocket port with: %d", serverPort);
blog(LOG_INFO, "[Config::Load] --websocket_port passed. Overriding WebSocket port with: %d", serverPort);
PortOverridden = true;
ServerPort = serverPort;
} else {
ServerPort = config_get_uint(obsConfig, CONFIG_SECTION_NAME, PARAM_PORT);
blog(LOG_WARNING, "[Config::Load] Not overriding WebSocket port since integer conversion failed.");
}
} else {
ServerPort = config_get_uint(obsConfig, CONFIG_SECTION_NAME, PARAM_PORT);
}
// Process `--websocket_password` override
QString passwordArgument = Utils::Platform::GetCommandLineArgument(CMDLINE_WEBSOCKET_PASSWORD);
if (passwordArgument != "") {
blog(LOG_INFO, "[Config::Load] Overriding websocket password");
blog(LOG_INFO, "[Config::Load] --websocket_password passed. Overriding WebSocket password.");
PasswordOverridden = true;
AuthRequired = true;
ServerPassword = passwordArgument;
} else {
AuthRequired = config_get_bool(obsConfig, CONFIG_SECTION_NAME, PARAM_AUTHREQUIRED);
if (FirstLoad) {
ServerPassword = Utils::Crypto::GeneratePassword();
} else {
ServerPassword = config_get_string(obsConfig, CONFIG_SECTION_NAME, PARAM_PASSWORD);
}
}
if (Utils::Platform::GetCommandLineFlagSet(CMDLINE_WEBSOCKET_DEBUG)) // Debug does not persist on reload, so we let people override it with a flag.
// Process `--websocket_debug` override
if (Utils::Platform::GetCommandLineFlagSet(CMDLINE_WEBSOCKET_DEBUG)) {
// Debug does not persist on reload, so we let people override it with a flag.
blog(LOG_INFO, "[Config::Load] --websocket_debug passed. Enabling debug logging.");
DebugEnabled = true;
}
}
void Config::Save()

View File

@ -1,27 +1,45 @@
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#pragma once
#include <atomic>
#include <QString>
#include <util/config-file.h>
class Config {
public:
Config();
void Load();
void Save();
void SetDefaultsToGlobalStore();
config_t* GetConfigStore();
#include "plugin-macros.generated.h"
bool PortOverridden;
bool PasswordOverridden;
struct Config {
Config();
void Load();
void Save();
void SetDefaultsToGlobalStore();
config_t* GetConfigStore();
bool FirstLoad;
bool ServerEnabled;
uint16_t ServerPort;
bool DebugEnabled;
bool AlertsEnabled;
bool AuthRequired;
QString ServerPassword;
private:
std::atomic<bool> PortOverridden;
std::atomic<bool> PasswordOverridden;
std::atomic<bool> FirstLoad;
std::atomic<bool> ServerEnabled;
std::atomic<uint16_t> ServerPort;
std::atomic<bool> DebugEnabled;
std::atomic<bool> AlertsEnabled;
std::atomic<bool> AuthRequired;
QString ServerPassword;
};

209
src/WebSocketApi.cpp Normal file
View File

@ -0,0 +1,209 @@
#include "WebSocketApi.h"
#include "obs-websocket.h"
#define RETURN_STATUS(status) { calldata_set_bool(cd, "success", status); return; }
#define RETURN_SUCCESS() RETURN_STATUS(true);
#define RETURN_FAILURE() RETURN_STATUS(false);
WebSocketApi::Vendor *get_vendor(calldata_t *cd)
{
void *voidVendor;
if (!calldata_get_ptr(cd, "vendor", &voidVendor)) {
blog(LOG_WARNING, "[WebSocketApi: get_vendor] Failed due to missing `vendor` pointer.");
return nullptr;
}
return static_cast<WebSocketApi::Vendor*>(voidVendor);
}
WebSocketApi::WebSocketApi()
{
blog_debug("[WebSocketApi::WebSocketApi] Setting up...");
_procHandler = proc_handler_create();
proc_handler_add(_procHandler, "bool vendor_register(in string name, out ptr vendor)", &vendor_register_cb, this);
proc_handler_add(_procHandler, "bool vendor_request_register(in ptr vendor, in string type, in ptr callback)", &vendor_request_register_cb, this);
proc_handler_add(_procHandler, "bool vendor_request_unregister(in ptr vendor, in string type)", &vendor_request_unregister_cb, this);
proc_handler_add(_procHandler, "bool vendor_event_emit(in ptr vendor, in string type, in ptr data)", &vendor_event_emit_cb, this);
proc_handler_t *ph = obs_get_proc_handler();
assert(ph != NULL);
proc_handler_add(ph, "bool obs_websocket_api_get_ph(out ptr ph)", &get_ph_cb, this);
blog_debug("[WebSocketApi::WebSocketApi] Finished.");
}
WebSocketApi::~WebSocketApi()
{
blog_debug("[WebSocketApi::~WebSocketApi] Shutting down...");
proc_handler_destroy(_procHandler);
for (auto vendor : _vendors) {
blog_debug("[WebSocketApi::~WebSocketApi] Deleting vendor: %s", vendor.first.c_str());
delete vendor.second;
}
blog_debug("[WebSocketApi::~WebSocketApi] Finished.");
}
void WebSocketApi::SetEventCallback(EventCallback cb)
{
_eventCallback = cb;
}
enum WebSocketApi::RequestReturnCode WebSocketApi::PerformVendorRequest(std::string vendorName, std::string requestType, obs_data_t *requestData, obs_data_t *responseData)
{
std::shared_lock l(_mutex);
if (_vendors.count(vendorName) == 0)
return RequestReturnCode::NoVendor;
auto v = _vendors[vendorName];
l.unlock();
std::shared_lock v_l(v->_mutex);
if (v->_requests.count(requestType) == 0)
return RequestReturnCode::NoVendorRequest;
auto cb = v->_requests[requestType];
v_l.unlock();
cb.callback(requestData, responseData, cb.priv_data);
return RequestReturnCode::Normal;
}
void WebSocketApi::get_ph_cb(void *priv_data, calldata_t *cd)
{
auto c = static_cast<WebSocketApi*>(priv_data);
calldata_set_ptr(cd, "ph", (void*)c->_procHandler);
RETURN_SUCCESS();
}
void WebSocketApi::vendor_register_cb(void *priv_data, calldata_t *cd)
{
auto c = static_cast<WebSocketApi*>(priv_data);
const char *vendorName;
if (!calldata_get_string(cd, "name", &vendorName) || strlen(vendorName) == 0) {
blog(LOG_WARNING, "[WebSocketApi::vendor_register_cb] Failed due to missing `name` string.");
RETURN_FAILURE();
}
// Theoretically doesn't need a mutex, but it's good to be safe.
std::unique_lock l(c->_mutex);
if (c->_vendors.count(vendorName)) {
blog(LOG_WARNING, "[WebSocketApi::vendor_register_cb] Failed because `%s` is already a registered vendor.", vendorName);
RETURN_FAILURE();
}
Vendor* v = new Vendor();
v->_name = vendorName;
c->_vendors[vendorName] = v;
blog_debug("[WebSocketApi::vendor_register_cb] [vendorName: %s] Registered new vendor.", v->_name.c_str());
calldata_set_ptr(cd, "vendor", static_cast<void*>(v));
RETURN_SUCCESS();
}
void WebSocketApi::vendor_request_register_cb(void *, calldata_t *cd)
{
Vendor *v = get_vendor(cd);
if (!v)
RETURN_FAILURE();
const char *requestType;
if (!calldata_get_string(cd, "type", &requestType) || strlen(requestType) == 0) {
blog(LOG_WARNING, "[WebSocketApi::vendor_request_register_cb] [vendorName: %s] Failed due to missing or empty `type` string.", v->_name.c_str());
RETURN_FAILURE();
}
void *voidCallback;
if (!calldata_get_ptr(cd, "callback", &voidCallback) || !voidCallback) {
blog(LOG_WARNING, "[WebSocketApi::vendor_request_register_cb] [vendorName: %s] Failed due to missing `callback` pointer.", v->_name.c_str());
RETURN_FAILURE();
}
auto cb = static_cast<obs_websocket_request_callback*>(voidCallback);
std::unique_lock l(v->_mutex);
if (v->_requests.count(requestType)) {
blog(LOG_WARNING, "[WebSocketApi::vendor_request_register_cb] [vendorName: %s] Failed because `%s` is already a registered request.", v->_name.c_str(), requestType);
RETURN_FAILURE();
}
v->_requests[requestType] = *cb;
blog_debug("[WebSocketApi::vendor_request_register_cb] [vendorName: %s] Registered new vendor request: %s", v->_name.c_str(), requestType);
RETURN_SUCCESS();
}
void WebSocketApi::vendor_request_unregister_cb(void *, calldata_t *cd)
{
Vendor *v = get_vendor(cd);
if (!v)
RETURN_FAILURE();
const char *requestType;
if (!calldata_get_string(cd, "type", &requestType) || strlen(requestType) == 0) {
blog(LOG_WARNING, "[WebSocketApi::vendor_request_unregister_cb] [vendorName: %s] Failed due to missing `type` string.", v->_name.c_str());
RETURN_FAILURE();
}
std::unique_lock l(v->_mutex);
if (!v->_requests.count(requestType)) {
blog(LOG_WARNING, "[WebSocketApi::vendor_request_register_cb] [vendorName: %s] Failed because `%s` is not a registered request.", v->_name.c_str(), requestType);
RETURN_FAILURE();
}
v->_requests.erase(requestType);
blog_debug("[WebSocketApi::vendor_request_unregister_cb] [vendorName: %s] Unregistered vendor request: %s", v->_name.c_str(), requestType);
RETURN_SUCCESS();
}
void WebSocketApi::vendor_event_emit_cb(void *priv_data, calldata_t *cd)
{
auto c = static_cast<WebSocketApi*>(priv_data);
Vendor *v = get_vendor(cd);
if (!v)
RETURN_FAILURE();
const char *eventType;
if (!calldata_get_string(cd, "type", &eventType) || strlen(eventType) == 0) {
blog(LOG_WARNING, "[WebSocketApi::vendor_event_emit_cb] [vendorName: %s] Failed due to missing `type` string.", v->_name.c_str());
RETURN_FAILURE();
}
void *voidEventData;
if (!calldata_get_ptr(cd, "data", &voidEventData)) {
blog(LOG_WARNING, "[WebSocketApi::vendor_event_emit_cb] [vendorName: %s] Failed due to missing `data` pointer.", v->_name.c_str());
RETURN_FAILURE();
}
auto eventData = static_cast<obs_data_t*>(voidEventData);
if (!c->_eventCallback)
RETURN_FAILURE();
c->_eventCallback(v->_name, eventType, eventData);
RETURN_SUCCESS();
}

46
src/WebSocketApi.h Normal file
View File

@ -0,0 +1,46 @@
#pragma once
#include <functional>
#include <string>
#include <map>
#include <mutex>
#include <shared_mutex>
#include <obs.h>
#include "../lib/obs-websocket-api.h"
class WebSocketApi {
public:
enum RequestReturnCode {
Normal,
NoVendor,
NoVendorRequest,
};
typedef std::function<void(std::string, std::string, obs_data_t*)> EventCallback;
struct Vendor {
std::shared_mutex _mutex;
std::string _name;
std::map<std::string, obs_websocket_request_callback> _requests;
};
WebSocketApi();
~WebSocketApi();
void SetEventCallback(EventCallback cb);
enum RequestReturnCode PerformVendorRequest(std::string vendorName, std::string requestName, obs_data_t *requestData, obs_data_t *responseData);
static void get_ph_cb(void *priv_data, calldata_t *cd);
static void vendor_register_cb(void *priv_data, calldata_t *cd);
static void vendor_request_register_cb(void *priv_data, calldata_t *cd);
static void vendor_request_unregister_cb(void *priv_data, calldata_t *cd);
static void vendor_event_emit_cb(void *priv_data, calldata_t *cd);
private:
std::shared_mutex _mutex;
EventCallback _eventCallback;
proc_handler_t *_procHandler;
std::map<std::string, Vendor*> _vendors;
};

View File

@ -1,245 +0,0 @@
#include <obs-module.h>
#include "WebSocketProtocol.h"
#include "WebSocketSession.h"
#include "requesthandler/RequestHandler.h"
#include "requesthandler/rpc/RequestStatus.h"
#include "obs-websocket.h"
#include "Config.h"
#include "plugin-macros.generated.h"
#include "utils/Crypto.h"
#include "utils/Json.h"
#include "utils/Platform.h"
bool IsSupportedRpcVersion(uint8_t requestedVersion)
{
for (auto version : WebSocketProtocol::SupportedRpcVersions) {
if (requestedVersion == version)
return true;
}
return false;
}
void SetSessionParameters(SessionPtr session, WebSocketProtocol::ProcessResult &ret, json payloadData)
{
if (payloadData.contains("ignoreInvalidMessages")) {
if (!payloadData["ignoreInvalidMessages"].is_boolean()) {
ret.closeCode = WebSocketServer::WebSocketCloseCode::InvalidDataKeyType;
ret.closeReason = "Your `ignoreInvalidMessages` is not a boolean.";
return;
}
session->SetIgnoreInvalidMessages(payloadData["ignoreInvalidMessages"]);
}
if (payloadData.contains("ignoreNonFatalRequestChecks")) {
if (!payloadData["ignoreNonFatalRequestChecks"].is_boolean()) {
ret.closeCode = WebSocketServer::WebSocketCloseCode::InvalidDataKeyType;
ret.closeReason = "Your `ignoreNonFatalRequestChecks` is not a boolean.";
return;
}
session->SetIgnoreNonFatalRequestChecks(payloadData["ignoreNonFatalRequestChecks"]);
}
if (payloadData.contains("eventSubscriptions")) {
if (!payloadData["eventSubscriptions"].is_number_unsigned()) {
ret.closeCode = WebSocketServer::WebSocketCloseCode::InvalidDataKeyType;
ret.closeReason = "Your `eventSubscriptions` is not an unsigned number.";
return;
}
session->SetEventSubscriptions(payloadData["eventSubscriptions"]);
}
}
void WebSocketProtocol::ProcessMessage(SessionPtr session, WebSocketProtocol::ProcessResult &ret, uint8_t opCode, json payloadData)
{
if (!payloadData.is_object()) {
if (payloadData.is_null()) {
ret.closeCode = WebSocketServer::WebSocketCloseCode::MissingDataKey;
ret.closeReason = "Your payload is missing data (`d`).";
} else {
ret.closeCode = WebSocketServer::WebSocketCloseCode::InvalidDataKeyType;
ret.closeReason = "Your payload's data (`d`) is not an object.";
}
return;
}
// Only `Identify` is allowed when not identified
if (!session->IsIdentified() && opCode != 1) {
ret.closeCode = WebSocketServer::WebSocketCloseCode::NotIdentified;
ret.closeReason = "You attempted to send a non-Identify message while not identified.";
return;
}
switch (opCode) {
case 1: { // Identify
std::unique_lock<std::mutex> sessionLock(session->OperationMutex);
if (session->IsIdentified()) {
if (!session->IgnoreInvalidMessages()) {
ret.closeCode = WebSocketServer::WebSocketCloseCode::AlreadyIdentified;
ret.closeReason = "You are already Identified with the obs-websocket server.";
}
return;
}
if (session->AuthenticationRequired()) {
if (!payloadData.contains("authentication")) {
ret.closeCode = WebSocketServer::WebSocketCloseCode::AuthenticationFailed;
ret.closeReason = "Your payload's data is missing an `authentication` string, however authentication is required.";
return;
}
if (!Utils::Crypto::CheckAuthenticationString(session->Secret(), session->Challenge(), payloadData["authentication"])) {
auto conf = GetConfig();
if (conf && conf->AlertsEnabled) {
QString title = obs_module_text("OBSWebSocket.TrayNotification.AuthenticationFailed.Title");
QString body = QString(obs_module_text("OBSWebSocket.TrayNotification.AuthenticationFailed.Body")).arg(QString::fromStdString(session->RemoteAddress()));
Utils::Platform::SendTrayNotification(QSystemTrayIcon::Warning, title, body);
}
ret.closeCode = WebSocketServer::WebSocketCloseCode::AuthenticationFailed;
ret.closeReason = "Authentication failed.";
return;
}
}
if (!payloadData.contains("rpcVersion")) {
ret.closeCode = WebSocketServer::WebSocketCloseCode::MissingDataKey;
ret.closeReason = "Your payload's data is missing an `rpcVersion`.";
return;
}
if (!payloadData["rpcVersion"].is_number_unsigned()) {
ret.closeCode = WebSocketServer::WebSocketCloseCode::InvalidDataKeyType;
ret.closeReason = "Your `rpcVersion` is not an unsigned number.";
}
uint8_t requestedRpcVersion = payloadData["rpcVersion"];
if (!IsSupportedRpcVersion(requestedRpcVersion)) {
ret.closeCode = WebSocketServer::WebSocketCloseCode::UnsupportedRpcVersion;
ret.closeReason = "Your requested RPC version is not supported by this server.";
return;
}
session->SetRpcVersion(requestedRpcVersion);
SetSessionParameters(session, ret, payloadData);
if (ret.closeCode != WebSocketServer::WebSocketCloseCode::DontClose) {
return;
}
session->SetIsIdentified(true);
auto conf = GetConfig();
if (conf && conf->AlertsEnabled) {
QString title = obs_module_text("OBSWebSocket.TrayNotification.Identified.Title");
QString body = QString(obs_module_text("OBSWebSocket.TrayNotification.Identified.Body")).arg(QString::fromStdString(session->RemoteAddress()));
Utils::Platform::SendTrayNotification(QSystemTrayIcon::Information, title, body);
}
ret.result["op"] = 2;
ret.result["d"]["negotiatedRpcVersion"] = session->RpcVersion();
} return;
case 3: { // Reidentify
std::unique_lock<std::mutex> sessionLock(session->OperationMutex);
SetSessionParameters(session, ret, payloadData);
if (ret.closeCode != WebSocketServer::WebSocketCloseCode::DontClose) {
return;
}
ret.result["op"] = 2;
ret.result["d"]["negotiatedRpcVersion"] = session->RpcVersion();
} return;
case 6: { // Request
// RequestID checking has to be done here where we are able to close the connection.
if (!payloadData.contains("requestId")) {
if (!session->IgnoreInvalidMessages()) {
ret.closeCode = WebSocketServer::WebSocketCloseCode::MissingDataKey;
ret.closeReason = "Your payload data is missing a `requestId`.";
}
return;
}
RequestHandler requestHandler;
Request request(session, payloadData["requestType"], payloadData["requestData"]);
RequestResult requestResult = requestHandler.ProcessRequest(request);
json resultPayloadData;
resultPayloadData["requestType"] = payloadData["requestType"];
resultPayloadData["requestId"] = payloadData["requestId"];
resultPayloadData["requestStatus"] = {
{"result", requestResult.StatusCode == RequestStatus::Success},
{"code", requestResult.StatusCode}
};
if (!requestResult.Comment.empty())
resultPayloadData["requestStatus"]["comment"] = requestResult.Comment;
if (requestResult.ResponseData.is_object())
resultPayloadData["responseData"] = requestResult.ResponseData;
ret.result["op"] = 7;
ret.result["d"] = resultPayloadData;
} return;
case 8: { // RequestBatch
// RequestID checking has to be done here where we are able to close the connection.
if (!payloadData.contains("requestId")) {
if (!session->IgnoreInvalidMessages()) {
ret.closeCode = WebSocketServer::WebSocketCloseCode::MissingDataKey;
ret.closeReason = "Your payload data is missing a `requestId`.";
}
return;
}
if (!payloadData.contains("requests")) {
if (!session->IgnoreInvalidMessages()) {
ret.closeCode = WebSocketServer::WebSocketCloseCode::MissingDataKey;
ret.closeReason = "Your payload data is missing a `requests`.";
}
return;
}
if (!payloadData["requests"].is_array()) {
if (!session->IgnoreInvalidMessages()) {
ret.closeCode = WebSocketServer::WebSocketCloseCode::InvalidDataKeyType;
ret.closeReason = "Your `requests` is not an array.";
}
return;
}
std::vector<json> requests = payloadData["requests"];
json results = json::array();
RequestHandler requestHandler;
for (auto requestJson : requests) {
Request request(session, requestJson["requestType"], requestJson["requestData"]);
RequestResult requestResult = requestHandler.ProcessRequest(request);
json result;
result["requestType"] = requestJson["requestType"];
if (requestJson.contains("requestId"))
result["requestId"] = requestJson["requestId"];
result["requestStatus"] = {
{"result", requestResult.StatusCode == RequestStatus::Success},
{"code", requestResult.StatusCode}
};
if (!requestResult.Comment.empty())
result["requestStatus"]["comment"] = requestResult.Comment;
if (requestResult.ResponseData.is_object())
result["responseData"] = requestResult.ResponseData;
results.push_back(result);
}
ret.result["op"] = 9;
ret.result["d"]["requestId"] = payloadData["requestId"];
ret.result["d"]["results"] = results;
} return;
default:
if (!session->IgnoreInvalidMessages()) {
ret.closeCode = WebSocketServer::WebSocketCloseCode::UnknownOpCode;
ret.closeReason = std::string("Unknown OpCode: %s") + std::to_string(opCode);
}
return;
}
}

View File

@ -1,23 +0,0 @@
#pragma once
#include <vector>
#include <string>
#include "WebSocketServer.h"
class WebSocketSession;
typedef std::shared_ptr<WebSocketSession> SessionPtr;
namespace WebSocketProtocol {
const std::vector<uint8_t> SupportedRpcVersions{
1
};
struct ProcessResult {
WebSocketServer::WebSocketCloseCode closeCode = WebSocketServer::WebSocketCloseCode::DontClose;
std::string closeReason;
json result;
};
void ProcessMessage(SessionPtr session, ProcessResult &ret, uint8_t opCode, json incomingMessage);
}

View File

@ -1,114 +0,0 @@
#pragma once
#include <mutex>
#include <QObject>
#include <QThreadPool>
#include <QString>
#include <websocketpp/config/asio_no_tls.hpp>
#include <websocketpp/server.hpp>
#include "utils/Json.h"
#include "WebSocketSession.h"
class WebSocketServer : QObject
{
Q_OBJECT
public:
enum WebSocketEncoding {
Json,
MsgPack
};
struct WebSocketSessionState {
websocketpp::connection_hdl hdl;
std::string remoteAddress;
uint64_t connectedAt;
uint64_t incomingMessages;
uint64_t outgoingMessages;
bool isIdentified;
};
enum WebSocketOpCode {
Hello = 0,
Identify = 1,
Identified = 2,
Reidentify = 3,
Event = 5,
Request = 6,
RequestResponse = 7,
RequestBatch = 8,
RequestBatchResponse = 9,
};
enum WebSocketCloseCode {
// Internal only
DontClose = 0,
// Reserved
UnknownReason = 4000,
// The requested `Content-Type` specified in the request HTTP header is invalid.
InvalidContentType = 4001,
// The server was unable to decode the incoming websocket message
MessageDecodeError = 4002,
// A data key is missing but required
MissingDataKey = 4003,
// A data key has an invalid type
InvalidDataKeyType = 4004,
// The specified `op` was invalid or missing
UnknownOpCode = 4005,
// The client sent a websocket message without first sending `Identify` message
NotIdentified = 4006,
// The client sent an `Identify` message while already identified
AlreadyIdentified = 4007,
// The authentication attempt (via `Identify`) failed
AuthenticationFailed = 4008,
// The server detected the usage of an old version of the obs-websocket RPC protocol.
UnsupportedRpcVersion = 4009,
// The websocket session has been invalidated by the obs-websocket server.
SessionInvalidated = 4010,
};
WebSocketServer();
~WebSocketServer();
void Start();
void Stop();
void InvalidateSession(websocketpp::connection_hdl hdl);
bool IsListening() {
return _server.is_listening();
}
std::vector<WebSocketSessionState> GetWebSocketSessions();
QThreadPool *GetThreadPool() {
return &_threadPool;
}
bool AuthenticationRequired;
std::string AuthenticationSecret;
std::string AuthenticationSalt;
public Q_SLOTS:
void BroadcastEvent(uint64_t requiredIntent, std::string eventType, json eventData = nullptr, uint8_t rpcVersion = 0);
signals:
void ClientConnected(const WebSocketSessionState state);
void ClientDisconnected(const WebSocketSessionState state, const uint16_t closeCode);
private:
void ServerRunner();
void onOpen(websocketpp::connection_hdl hdl);
void onClose(websocketpp::connection_hdl hdl);
void onMessage(websocketpp::connection_hdl hdl, websocketpp::server<websocketpp::config::asio>::message_ptr message);
std::thread _serverThread;
websocketpp::server<websocketpp::config::asio> _server;
QThreadPool _threadPool;
std::mutex _sessionMutex;
std::map<websocketpp::connection_hdl, SessionPtr, std::owner_less<websocketpp::connection_hdl>> _sessions;
uint16_t _serverPort;
QString _serverPassword;
bool _debugEnabled;
};

View File

@ -1,11 +1,32 @@
#include "EventHandler.h"
#include "../plugin-macros.generated.h"
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
EventHandler::EventHandler(WebSocketServerPtr webSocketServer) :
_webSocketServer(webSocketServer),
_obsLoaded(false)
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#include "EventHandler.h"
EventHandler::EventHandler() :
_obsLoaded(false),
_inputVolumeMetersRef(0),
_inputActiveStateChangedRef(0),
_inputShowStateChangedRef(0),
_sceneItemTransformChangedRef(0)
{
blog(LOG_INFO, "[EventHandler::EventHandler] Setting up event handlers...");
blog_debug("[EventHandler::EventHandler] Setting up...");
obs_frontend_add_event_callback(OnFrontendEvent, this);
@ -19,12 +40,12 @@ EventHandler::EventHandler(WebSocketServerPtr webSocketServer) :
blog(LOG_ERROR, "[EventHandler::EventHandler] Unable to get libobs signal handler!");
}
blog(LOG_INFO, "[EventHandler::EventHandler] Finished.");
blog_debug("[EventHandler::EventHandler] Finished.");
}
EventHandler::~EventHandler()
{
blog(LOG_INFO, "[EventHandler::~EventHandler] Removing event handlers...");
blog_debug("[EventHandler::~EventHandler] Shutting down...");
obs_frontend_remove_event_callback(OnFrontendEvent, this);
@ -38,9 +59,63 @@ EventHandler::~EventHandler()
blog(LOG_ERROR, "[EventHandler::~EventHandler] Unable to get libobs signal handler!");
}
blog(LOG_INFO, "[EventHandler::~EventHandler] Finished.");
blog_debug("[EventHandler::~EventHandler] Finished.");
}
void EventHandler::SetBroadcastCallback(EventHandler::BroadcastCallback cb)
{
_broadcastCallback = cb;
}
void EventHandler::SetObsLoadedCallback(EventHandler::ObsLoadedCallback cb)
{
_obsLoadedCallback = cb;
}
// Function to increment refcounts for high volume event subscriptions
void EventHandler::ProcessSubscription(uint64_t eventSubscriptions)
{
if ((eventSubscriptions & EventSubscription::InputVolumeMeters) != 0) {
if (_inputVolumeMetersRef.fetch_add(1) == 0) {
if (_inputVolumeMetersHandler)
blog(LOG_WARNING, "[EventHandler::ProcessSubscription] Input volume meter handler already exists!");
else
_inputVolumeMetersHandler = std::make_unique<Utils::Obs::VolumeMeter::Handler>(std::bind(&EventHandler::HandleInputVolumeMeters, this, std::placeholders::_1));
}
}
if ((eventSubscriptions & EventSubscription::InputActiveStateChanged) != 0)
_inputActiveStateChangedRef++;
if ((eventSubscriptions & EventSubscription::InputShowStateChanged) != 0)
_inputShowStateChangedRef++;
if ((eventSubscriptions & EventSubscription::SceneItemTransformChanged) != 0)
_sceneItemTransformChangedRef++;
}
// Function to decrement refcounts for high volume event subscriptions
void EventHandler::ProcessUnsubscription(uint64_t eventSubscriptions)
{
if ((eventSubscriptions & EventSubscription::InputVolumeMeters) != 0) {
if (_inputVolumeMetersRef.fetch_sub(1) == 1)
_inputVolumeMetersHandler.reset();
}
if ((eventSubscriptions & EventSubscription::InputActiveStateChanged) != 0)
_inputActiveStateChangedRef--;
if ((eventSubscriptions & EventSubscription::InputShowStateChanged) != 0)
_inputShowStateChangedRef--;
if ((eventSubscriptions & EventSubscription::SceneItemTransformChanged) != 0)
_sceneItemTransformChangedRef--;
}
// Function required in order to use default arguments
void EventHandler::BroadcastEvent(uint64_t requiredIntent, std::string eventType, json eventData, uint8_t rpcVersion)
{
if (!_broadcastCallback)
return;
_broadcastCallback(requiredIntent, eventType, eventData, rpcVersion);
}
// Connect source signals for Inputs, Scenes, and Transitions. Filters are automatically connected.
void EventHandler::ConnectSourceSignals(obs_source_t *source) // Applies to inputs and scenes
{
if (!source || obs_source_removed(source))
@ -54,16 +129,17 @@ void EventHandler::ConnectSourceSignals(obs_source_t *source) // Applies to inpu
obs_source_type sourceType = obs_source_get_type(source);
// Inputs
signal_handler_connect(sh, "activate", HandleInputActiveStateChanged, this);
signal_handler_connect(sh, "deactivate", HandleInputActiveStateChanged, this);
signal_handler_connect(sh, "show", HandleInputShowStateChanged, this);
signal_handler_connect(sh, "hide", HandleInputShowStateChanged, this);
signal_handler_connect(sh, "mute", HandleInputMuteStateChanged, this);
signal_handler_connect(sh, "volume", HandleInputVolumeChanged, this);
signal_handler_connect(sh, "audio_sync", HandleInputAudioSyncOffsetChanged, this);
signal_handler_connect(sh, "audio_mixers", HandleInputAudioTracksChanged, this);
if (sourceType == OBS_SOURCE_TYPE_INPUT) {
signal_handler_connect(sh, "activate", HandleInputActiveStateChanged, this);
signal_handler_connect(sh, "deactivate", HandleInputActiveStateChanged, this);
signal_handler_connect(sh, "show", HandleInputShowStateChanged, this);
signal_handler_connect(sh, "hide", HandleInputShowStateChanged, this);
signal_handler_connect(sh, "mute", HandleInputMuteStateChanged, this);
signal_handler_connect(sh, "volume", HandleInputVolumeChanged, this);
signal_handler_connect(sh, "audio_balance", HandleInputAudioBalanceChanged, this);
signal_handler_connect(sh, "audio_sync", HandleInputAudioSyncOffsetChanged, this);
signal_handler_connect(sh, "audio_mixers", HandleInputAudioTracksChanged, this);
signal_handler_connect(sh, "audio_monitoring", HandleInputAudioMonitorTypeChanged, this);
signal_handler_connect(sh, "media_started", HandleMediaInputPlaybackStarted, this);
signal_handler_connect(sh, "media_ended", HandleMediaInputPlaybackEnded, this);
signal_handler_connect(sh, "media_pause", SourceMediaPauseMultiHandler, this);
@ -81,10 +157,37 @@ void EventHandler::ConnectSourceSignals(obs_source_t *source) // Applies to inpu
signal_handler_connect(sh, "reorder", HandleSceneItemListReindexed, this);
signal_handler_connect(sh, "item_visible", HandleSceneItemEnableStateChanged, this);
signal_handler_connect(sh, "item_locked", HandleSceneItemLockStateChanged, this);
signal_handler_connect(sh, "item_select", HandleSceneItemSelected, this);
signal_handler_connect(sh, "item_transform", HandleSceneItemTransformChanged, this);
}
// Scenes and Inputs
if (sourceType == OBS_SOURCE_TYPE_INPUT || sourceType == OBS_SOURCE_TYPE_SCENE) {
signal_handler_connect(sh, "reorder_filters", HandleSourceFilterListReindexed, this);
signal_handler_connect(sh, "filter_add", FilterAddMultiHandler, this);
signal_handler_connect(sh, "filter_remove", FilterRemoveMultiHandler, this);
auto enumFilters = [](obs_source_t *, obs_source_t *filter, void *param){
auto eventHandler = static_cast<EventHandler*>(param);
eventHandler->ConnectSourceSignals(filter);
};
obs_source_enum_filters(source, enumFilters, this);
}
// Transitions
if (sourceType == OBS_SOURCE_TYPE_TRANSITION) {
signal_handler_connect(sh, "transition_start", HandleSceneTransitionStarted, this);
signal_handler_connect(sh, "transition_stop", HandleSceneTransitionEnded, this);
signal_handler_connect(sh, "transition_video_stop", HandleSceneTransitionVideoEnded, this);
}
// Filters
if (sourceType == OBS_SOURCE_TYPE_FILTER) {
signal_handler_connect(sh, "enable", HandleSourceFilterEnableStateChanged, this);
signal_handler_connect(sh, "rename", HandleSourceFilterNameChanged, this);
}
}
// Disconnect source signals for Inputs, Scenes, and Transitions. Filters are automatically disconnected.
void EventHandler::DisconnectSourceSignals(obs_source_t *source)
{
if (!source)
@ -92,89 +195,160 @@ void EventHandler::DisconnectSourceSignals(obs_source_t *source)
signal_handler_t* sh = obs_source_get_signal_handler(source);
obs_source_type sourceType = obs_source_get_type(source);
// Inputs
signal_handler_disconnect(sh, "activate", HandleInputActiveStateChanged, this);
signal_handler_disconnect(sh, "deactivate", HandleInputActiveStateChanged, this);
signal_handler_disconnect(sh, "show", HandleInputShowStateChanged, this);
signal_handler_disconnect(sh, "hide", HandleInputShowStateChanged, this);
signal_handler_disconnect(sh, "mute", HandleInputMuteStateChanged, this);
signal_handler_disconnect(sh, "volume", HandleInputVolumeChanged, this);
signal_handler_disconnect(sh, "audio_sync", HandleInputAudioSyncOffsetChanged, this);
signal_handler_disconnect(sh, "audio_mixers", HandleInputAudioTracksChanged, this);
signal_handler_disconnect(sh, "media_started", HandleMediaInputPlaybackStarted, this);
signal_handler_disconnect(sh, "media_ended", HandleMediaInputPlaybackEnded, this);
signal_handler_disconnect(sh, "media_pause", SourceMediaPauseMultiHandler, this);
signal_handler_disconnect(sh, "media_play", SourceMediaPlayMultiHandler, this);
signal_handler_disconnect(sh, "media_restart", SourceMediaRestartMultiHandler, this);
signal_handler_disconnect(sh, "media_stopped", SourceMediaStopMultiHandler, this);
signal_handler_disconnect(sh, "media_next", SourceMediaNextMultiHandler, this);
signal_handler_disconnect(sh, "media_previous", SourceMediaPreviousMultiHandler, this);
if (sourceType == OBS_SOURCE_TYPE_INPUT) {
signal_handler_disconnect(sh, "activate", HandleInputActiveStateChanged, this);
signal_handler_disconnect(sh, "deactivate", HandleInputActiveStateChanged, this);
signal_handler_disconnect(sh, "show", HandleInputShowStateChanged, this);
signal_handler_disconnect(sh, "hide", HandleInputShowStateChanged, this);
signal_handler_disconnect(sh, "mute", HandleInputMuteStateChanged, this);
signal_handler_disconnect(sh, "volume", HandleInputVolumeChanged, this);
signal_handler_disconnect(sh, "audio_balance", HandleInputAudioBalanceChanged, this);
signal_handler_disconnect(sh, "audio_sync", HandleInputAudioSyncOffsetChanged, this);
signal_handler_disconnect(sh, "audio_mixers", HandleInputAudioTracksChanged, this);
signal_handler_disconnect(sh, "audio_monitoring", HandleInputAudioMonitorTypeChanged, this);
signal_handler_disconnect(sh, "media_started", HandleMediaInputPlaybackStarted, this);
signal_handler_disconnect(sh, "media_ended", HandleMediaInputPlaybackEnded, this);
signal_handler_disconnect(sh, "media_pause", SourceMediaPauseMultiHandler, this);
signal_handler_disconnect(sh, "media_play", SourceMediaPlayMultiHandler, this);
signal_handler_disconnect(sh, "media_restart", SourceMediaRestartMultiHandler, this);
signal_handler_disconnect(sh, "media_stopped", SourceMediaStopMultiHandler, this);
signal_handler_disconnect(sh, "media_next", SourceMediaNextMultiHandler, this);
signal_handler_disconnect(sh, "media_previous", SourceMediaPreviousMultiHandler, this);
}
// Scenes
signal_handler_disconnect(sh, "item_add", HandleSceneItemCreated, this);
signal_handler_disconnect(sh, "item_remove", HandleSceneItemRemoved, this);
signal_handler_disconnect(sh, "reorder", HandleSceneItemListReindexed, this);
signal_handler_disconnect(sh, "item_visible", HandleSceneItemEnableStateChanged, this);
signal_handler_disconnect(sh, "item_locked", HandleSceneItemLockStateChanged, this);
signal_handler_disconnect(sh, "item_transform", HandleSceneItemTransformChanged, this);
if (sourceType == OBS_SOURCE_TYPE_SCENE) {
signal_handler_disconnect(sh, "item_add", HandleSceneItemCreated, this);
signal_handler_disconnect(sh, "item_remove", HandleSceneItemRemoved, this);
signal_handler_disconnect(sh, "reorder", HandleSceneItemListReindexed, this);
signal_handler_disconnect(sh, "item_visible", HandleSceneItemEnableStateChanged, this);
signal_handler_disconnect(sh, "item_locked", HandleSceneItemLockStateChanged, this);
signal_handler_disconnect(sh, "item_select", HandleSceneItemSelected, this);
signal_handler_disconnect(sh, "item_transform", HandleSceneItemTransformChanged, this);
}
// Inputs and Scenes
if (sourceType == OBS_SOURCE_TYPE_INPUT || sourceType == OBS_SOURCE_TYPE_SCENE) {
signal_handler_disconnect(sh, "reorder_filters", HandleSourceFilterListReindexed, this);
signal_handler_disconnect(sh, "filter_add", FilterAddMultiHandler, this);
signal_handler_disconnect(sh, "filter_remove", FilterRemoveMultiHandler, this);
auto enumFilters = [](obs_source_t *, obs_source_t *filter, void *param){
auto eventHandler = static_cast<EventHandler*>(param);
eventHandler->DisconnectSourceSignals(filter);
};
obs_source_enum_filters(source, enumFilters, this);
}
// Transitions
if (sourceType == OBS_SOURCE_TYPE_TRANSITION) {
signal_handler_disconnect(sh, "transition_start", HandleSceneTransitionStarted, this);
signal_handler_disconnect(sh, "transition_stop", HandleSceneTransitionEnded, this);
signal_handler_disconnect(sh, "transition_video_stop", HandleSceneTransitionVideoEnded, this);
}
// Filters
if (sourceType == OBS_SOURCE_TYPE_FILTER) {
signal_handler_disconnect(sh, "enable", HandleSourceFilterEnableStateChanged, this);
signal_handler_disconnect(sh, "rename", HandleSourceFilterNameChanged, this);
}
}
void EventHandler::OnFrontendEvent(enum obs_frontend_event event, void *private_data)
{
auto eventHandler = reinterpret_cast<EventHandler*>(private_data);
auto eventHandler = static_cast<EventHandler*>(private_data);
if (!eventHandler->_obsLoaded.load() && event != OBS_FRONTEND_EVENT_FINISHED_LOADING)
return;
switch (event) {
// General
case OBS_FRONTEND_EVENT_FINISHED_LOADING:
blog_debug("[EventHandler::OnFrontendEvent] OBS has finished loading. Connecting final handlers and enabling events...");
if (!eventHandler->_obsLoaded.load()) {
if (event == OBS_FRONTEND_EVENT_FINISHED_LOADING) {
blog(LOG_INFO, "[EventHandler::OnFrontendEvent] OBS has finished loading. Connecting final handlers and enabling events...");
// Connect source signals and enable events only after OBS has fully loaded (to reduce extra logging).
eventHandler->_obsLoaded.store(true);
// In the case that plugins become hotloadable, this will have to go back into `EventHandler::EventHandler()`
// Enumerate inputs and connect each one
obs_enum_sources([](void* param, obs_source_t* source) {
auto eventHandler = reinterpret_cast<EventHandler*>(param);
eventHandler->ConnectSourceSignals(source);
return true;
}, private_data);
{
auto enumInputs = [](void *param, obs_source_t *source) {
auto eventHandler = static_cast<EventHandler*>(param);
eventHandler->ConnectSourceSignals(source);
return true;
};
obs_enum_sources(enumInputs, private_data);
}
// Enumerate scenes and connect each one
obs_enum_scenes([](void* param, obs_source_t* source) {
auto eventHandler = reinterpret_cast<EventHandler*>(param);
eventHandler->ConnectSourceSignals(source);
return true;
}, private_data);
{
auto enumScenes = [](void *param, obs_source_t *source) {
auto eventHandler = static_cast<EventHandler*>(param);
eventHandler->ConnectSourceSignals(source);
return true;
};
obs_enum_scenes(enumScenes, private_data);
}
blog(LOG_INFO, "[EventHandler::OnFrontendEvent] Finished.");
} else {
return;
}
}
// Enumerate all scene transitions and connect each one
{
obs_frontend_source_list transitions = {};
obs_frontend_get_transitions(&transitions);
for (size_t i = 0; i < transitions.sources.num; i++) {
obs_source_t* transition = transitions.sources.array[i];
eventHandler->ConnectSourceSignals(transition);
}
obs_frontend_source_list_free(&transitions);
}
switch (event) {
// General
blog_debug("[EventHandler::OnFrontendEvent] Finished.");
if (eventHandler->_obsLoadedCallback)
eventHandler->_obsLoadedCallback();
break;
case OBS_FRONTEND_EVENT_EXIT:
eventHandler->HandleExitStarted();
blog(LOG_INFO, "[EventHandler::OnFrontendEvent] OBS is unloading. Disabling events...");
blog_debug("[EventHandler::OnFrontendEvent] OBS is unloading. Disabling events...");
// Disconnect source signals and disable events when OBS starts unloading (to reduce extra logging).
eventHandler->_obsLoaded.store(false);
// In the case that plugins become hotloadable, this will have to go back into `EventHandler::~EventHandler()`
// Enumerate inputs and disconnect each one
obs_enum_sources([](void* param, obs_source_t* source) {
auto eventHandler = reinterpret_cast<EventHandler*>(param);
eventHandler->DisconnectSourceSignals(source);
return true;
}, private_data);
{
auto enumInputs = [](void *param, obs_source_t *source) {
auto eventHandler = static_cast<EventHandler*>(param);
eventHandler->DisconnectSourceSignals(source);
return true;
};
obs_enum_sources(enumInputs, private_data);
}
// Enumerate scenes and disconnect each one
obs_enum_scenes([](void* param, obs_source_t* source) {
auto eventHandler = reinterpret_cast<EventHandler*>(param);
eventHandler->DisconnectSourceSignals(source);
return true;
}, private_data);
{
auto enumScenes = [](void *param, obs_source_t *source) {
auto eventHandler = static_cast<EventHandler*>(param);
eventHandler->DisconnectSourceSignals(source);
return true;
};
obs_enum_scenes(enumScenes, private_data);
}
blog(LOG_INFO, "[EventHandler::OnFrontendEvent] Finished.");
// Enumerate all scene transitions and disconnect each one
{
obs_frontend_source_list transitions = {};
obs_frontend_get_transitions(&transitions);
for (size_t i = 0; i < transitions.sources.num; i++) {
obs_source_t* transition = transitions.sources.array[i];
eventHandler->DisconnectSourceSignals(transition);
}
obs_frontend_source_list_free(&transitions);
}
blog_debug("[EventHandler::OnFrontendEvent] Finished.");
break;
case OBS_FRONTEND_EVENT_STUDIO_MODE_ENABLED:
@ -185,12 +359,36 @@ void EventHandler::OnFrontendEvent(enum obs_frontend_event event, void *private_
break;
// Config
case OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGING:
{
obs_frontend_source_list transitions = {};
obs_frontend_get_transitions(&transitions);
for (size_t i = 0; i < transitions.sources.num; i++) {
obs_source_t* transition = transitions.sources.array[i];
eventHandler->DisconnectSourceSignals(transition);
}
obs_frontend_source_list_free(&transitions);
}
eventHandler->HandleCurrentSceneCollectionChanging();
break;
case OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED:
{
obs_frontend_source_list transitions = {};
obs_frontend_get_transitions(&transitions);
for (size_t i = 0; i < transitions.sources.num; i++) {
obs_source_t* transition = transitions.sources.array[i];
eventHandler->ConnectSourceSignals(transition);
}
obs_frontend_source_list_free(&transitions);
}
eventHandler->HandleCurrentSceneCollectionChanged();
break;
case OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED:
eventHandler->HandleSceneCollectionListChanged();
break;
case OBS_FRONTEND_EVENT_PROFILE_CHANGING:
eventHandler->HandleCurrentProfileChanging();
break;
case OBS_FRONTEND_EVENT_PROFILE_CHANGED:
eventHandler->HandleCurrentProfileChanged();
break;
@ -200,7 +398,7 @@ void EventHandler::OnFrontendEvent(enum obs_frontend_event event, void *private_
// Scenes
case OBS_FRONTEND_EVENT_SCENE_CHANGED:
eventHandler->HandleCurrentSceneChanged();
eventHandler->HandleCurrentProgramSceneChanged();
break;
case OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED:
eventHandler->HandleCurrentPreviewSceneChanged();
@ -211,10 +409,21 @@ void EventHandler::OnFrontendEvent(enum obs_frontend_event event, void *private_
// Transitions
case OBS_FRONTEND_EVENT_TRANSITION_CHANGED:
eventHandler->HandleCurrentSceneTransitionChanged();
break;
case OBS_FRONTEND_EVENT_TRANSITION_LIST_CHANGED:
{
obs_frontend_source_list transitions = {};
obs_frontend_get_transitions(&transitions);
for (size_t i = 0; i < transitions.sources.num; i++) {
obs_source_t* transition = transitions.sources.array[i];
eventHandler->ConnectSourceSignals(transition);
}
obs_frontend_source_list_free(&transitions);
}
break;
case OBS_FRONTEND_EVENT_TRANSITION_DURATION_CHANGED:
eventHandler->HandleCurrentSceneTransitionDurationChanged();
break;
// Outputs
@ -242,6 +451,12 @@ void EventHandler::OnFrontendEvent(enum obs_frontend_event event, void *private_
case OBS_FRONTEND_EVENT_RECORDING_STOPPED:
eventHandler->HandleRecordStateChanged(OBS_WEBSOCKET_OUTPUT_STOPPED);
break;
case OBS_FRONTEND_EVENT_RECORDING_PAUSED:
eventHandler->HandleRecordStateChanged(OBS_WEBSOCKET_OUTPUT_PAUSED);
break;
case OBS_FRONTEND_EVENT_RECORDING_UNPAUSED:
eventHandler->HandleRecordStateChanged(OBS_WEBSOCKET_OUTPUT_RESUMED);
break;
case OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING:
eventHandler->HandleReplayBufferStateChanged(OBS_WEBSOCKET_OUTPUT_STARTING);
break;
@ -272,7 +487,7 @@ void EventHandler::OnFrontendEvent(enum obs_frontend_event event, void *private_
// Only called for creation of a public source
void EventHandler::SourceCreatedMultiHandler(void *param, calldata_t *data)
{
auto eventHandler = reinterpret_cast<EventHandler*>(param);
auto eventHandler = static_cast<EventHandler*>(param);
// Don't react to signals until OBS has finished loading
if (!eventHandler->_obsLoaded.load())
@ -288,10 +503,6 @@ void EventHandler::SourceCreatedMultiHandler(void *param, calldata_t *data)
case OBS_SOURCE_TYPE_INPUT:
eventHandler->HandleInputCreated(source);
break;
case OBS_SOURCE_TYPE_FILTER:
break;
case OBS_SOURCE_TYPE_TRANSITION:
break;
case OBS_SOURCE_TYPE_SCENE:
eventHandler->HandleSceneCreated(source);
break;
@ -303,7 +514,7 @@ void EventHandler::SourceCreatedMultiHandler(void *param, calldata_t *data)
// Only called for destruction of a public source
void EventHandler::SourceDestroyedMultiHandler(void *param, calldata_t *data)
{
auto eventHandler = reinterpret_cast<EventHandler*>(param);
auto eventHandler = static_cast<EventHandler*>(param);
// We can't use any smart types here because releasing the source will cause infinite recursion
obs_source_t *source = GetCalldataPointer<obs_source_t>(data, "source");
@ -322,10 +533,6 @@ void EventHandler::SourceDestroyedMultiHandler(void *param, calldata_t *data)
// We have to call `InputRemoved` with source_destroy because source_removed is not called when an input's last scene item is removed
eventHandler->HandleInputRemoved(source);
break;
case OBS_SOURCE_TYPE_FILTER:
break;
case OBS_SOURCE_TYPE_TRANSITION:
break;
case OBS_SOURCE_TYPE_SCENE:
break;
default:
@ -335,7 +542,7 @@ void EventHandler::SourceDestroyedMultiHandler(void *param, calldata_t *data)
void EventHandler::SourceRemovedMultiHandler(void *param, calldata_t *data)
{
auto eventHandler = reinterpret_cast<EventHandler*>(param);
auto eventHandler = static_cast<EventHandler*>(param);
if (!eventHandler->_obsLoaded.load())
return;
@ -347,10 +554,6 @@ void EventHandler::SourceRemovedMultiHandler(void *param, calldata_t *data)
switch (obs_source_get_type(source)) {
case OBS_SOURCE_TYPE_INPUT:
break;
case OBS_SOURCE_TYPE_FILTER:
break;
case OBS_SOURCE_TYPE_TRANSITION:
break;
case OBS_SOURCE_TYPE_SCENE:
// Scenes emit the `removed` signal when they are removed from OBS, as expected
eventHandler->HandleSceneRemoved(source);
@ -362,7 +565,7 @@ void EventHandler::SourceRemovedMultiHandler(void *param, calldata_t *data)
void EventHandler::SourceRenamedMultiHandler(void *param, calldata_t *data)
{
auto eventHandler = reinterpret_cast<EventHandler*>(param);
auto eventHandler = static_cast<EventHandler*>(param);
if (!eventHandler->_obsLoaded.load())
return;
@ -380,8 +583,6 @@ void EventHandler::SourceRenamedMultiHandler(void *param, calldata_t *data)
case OBS_SOURCE_TYPE_INPUT:
eventHandler->HandleInputNameChanged(source, oldSourceName, sourceName);
break;
case OBS_SOURCE_TYPE_FILTER:
break;
case OBS_SOURCE_TYPE_TRANSITION:
break;
case OBS_SOURCE_TYPE_SCENE:

View File

@ -1,34 +1,65 @@
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#pragma once
#include <atomic>
#include <obs.hpp>
#include <obs-frontend-api.h>
#include <util/platform.h>
#include "types/EventSubscription.h"
#include "../obs-websocket.h"
#include "../WebSocketServer.h"
#include "../utils/Obs.h"
template <typename T> T* GetCalldataPointer(const calldata_t *data, const char* name) {
void *ptr = nullptr;
calldata_get_ptr(data, name, &ptr);
return reinterpret_cast<T*>(ptr);
}
#include "../utils/Obs_VolumeMeter.h"
#include "../plugin-macros.generated.h"
class EventHandler
{
public:
EventHandler(WebSocketServerPtr webSocketServer);
EventHandler();
~EventHandler();
typedef std::function<void(uint64_t, std::string, json, uint8_t)> BroadcastCallback;
void SetBroadcastCallback(BroadcastCallback cb);
typedef std::function<void()> ObsLoadedCallback;
void SetObsLoadedCallback(ObsLoadedCallback cb);
void ProcessSubscription(uint64_t eventSubscriptions);
void ProcessUnsubscription(uint64_t eventSubscriptions);
private:
WebSocketServerPtr _webSocketServer;
BroadcastCallback _broadcastCallback;
ObsLoadedCallback _obsLoadedCallback;
std::atomic<bool> _obsLoaded;
std::unique_ptr<Utils::Obs::VolumeMeter::Handler> _inputVolumeMetersHandler;
std::atomic<uint64_t> _inputVolumeMetersRef;
std::atomic<uint64_t> _inputActiveStateChangedRef;
std::atomic<uint64_t> _inputShowStateChangedRef;
std::atomic<uint64_t> _sceneItemTransformChangedRef;
void ConnectSourceSignals(obs_source_t *source);
void DisconnectSourceSignals(obs_source_t *source);
void BroadcastEvent(uint64_t requiredIntent, std::string eventType, json eventData = nullptr, uint8_t rpcVersion = 0);
// Signal handler: frontend
static void OnFrontendEvent(enum obs_frontend_event event, void *private_data);
@ -52,8 +83,10 @@ class EventHandler
void HandleStudioModeStateChanged(bool enabled);
// Config
void HandleCurrentSceneCollectionChanging();
void HandleCurrentSceneCollectionChanged();
void HandleSceneCollectionListChanged();
void HandleCurrentProfileChanging();
void HandleCurrentProfileChanged();
void HandleProfileListChanged();
@ -61,7 +94,7 @@ class EventHandler
void HandleSceneCreated(obs_source_t *source);
void HandleSceneRemoved(obs_source_t *source);
void HandleSceneNameChanged(obs_source_t *source, std::string oldSceneName, std::string sceneName);
void HandleCurrentSceneChanged();
void HandleCurrentProgramSceneChanged();
void HandleCurrentPreviewSceneChanged();
void HandleSceneListChanged();
@ -69,17 +102,31 @@ class EventHandler
void HandleInputCreated(obs_source_t *source);
void HandleInputRemoved(obs_source_t *source);
void HandleInputNameChanged(obs_source_t *source, std::string oldInputName, std::string inputName);
void HandleInputVolumeMeters(std::vector<json> inputs); // AudioMeter::Handler callback
static void HandleInputActiveStateChanged(void *param, calldata_t *data); // Direct callback
static void HandleInputShowStateChanged(void *param, calldata_t *data); // Direct callback
static void HandleInputMuteStateChanged(void *param, calldata_t *data); // Direct callback
static void HandleInputVolumeChanged(void *param, calldata_t *data); // Direct callback
static void HandleInputAudioBalanceChanged(void *param, calldata_t *data); // Direct callback
static void HandleInputAudioSyncOffsetChanged(void *param, calldata_t *data); // Direct callback
static void HandleInputAudioTracksChanged(void *param, calldata_t *data); // Direct callback
static void HandleInputAudioMonitorTypeChanged(void *param, calldata_t *data); // Direct callback
// Transitions
void HandleTransitionCreated(obs_source_t *source);
void HandleTransitionRemoved(obs_source_t *source);
void HandleTransitionNameChanged(obs_source_t *source, std::string oldTransitionName, std::string transitionName);
void HandleCurrentSceneTransitionChanged();
void HandleCurrentSceneTransitionDurationChanged();
static void HandleSceneTransitionStarted(void *param, calldata_t *data); // Direct callback
static void HandleSceneTransitionEnded(void *param, calldata_t *data); // Direct callback
static void HandleSceneTransitionVideoEnded(void *param, calldata_t *data); // Direct callback
// Filters
static void FilterAddMultiHandler(void *param, calldata_t *data); // Direct callback
static void FilterRemoveMultiHandler(void *param, calldata_t *data); // Direct callback
static void HandleSourceFilterListReindexed(void *param, calldata_t *data); // Direct callback
void HandleSourceFilterCreated(obs_source_t *source, obs_source_t *filter);
void HandleSourceFilterRemoved(obs_source_t *source, obs_source_t *filter);
static void HandleSourceFilterNameChanged(void *param, calldata_t *data); // Direct callback
static void HandleSourceFilterEnableStateChanged(void *param, calldata_t *data); // Direct callback
// Outputs
void HandleStreamStateChanged(ObsOutputState state);
@ -94,6 +141,7 @@ class EventHandler
static void HandleSceneItemListReindexed(void *param, calldata_t *data); // Direct callback
static void HandleSceneItemEnableStateChanged(void *param, calldata_t *data); // Direct callback
static void HandleSceneItemLockStateChanged(void *param, calldata_t *data); // Direct callback
static void HandleSceneItemSelected(void *param, calldata_t *data); // Direct callback
static void HandleSceneItemTransformChanged(void *param, calldata_t *data); // Direct callback
// Media Inputs

View File

@ -1,30 +1,145 @@
#include "EventHandler.h"
#include "../plugin-macros.generated.h"
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#include "EventHandler.h"
/**
* The current scene collection has begun changing.
*
* Note: We recommend using this event to trigger a pause of all polling requests, as performing any requests during a
* scene collection change is considered undefined behavior and can cause crashes!
*
* @dataField sceneCollectionName | String | Name of the current scene collection
*
* @eventType CurrentSceneCollectionChanging
* @eventSubscription Config
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api events
*/
void EventHandler::HandleCurrentSceneCollectionChanging()
{
json eventData;
eventData["sceneCollectionName"] = Utils::Obs::StringHelper::GetCurrentSceneCollection();
BroadcastEvent(EventSubscription::Config, "CurrentSceneCollectionChanging", eventData);
}
/**
* The current scene collection has changed.
*
* Note: If polling has been paused during `CurrentSceneCollectionChanging`, this is the que to restart polling.
*
* @dataField sceneCollectionName | String | Name of the new scene collection
*
* @eventType CurrentSceneCollectionChanged
* @eventSubscription Config
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api events
*/
void EventHandler::HandleCurrentSceneCollectionChanged()
{
json eventData;
eventData["sceneCollectionName"] = Utils::Obs::StringHelper::GetCurrentSceneCollection();
_webSocketServer->BroadcastEvent(EventSubscription::Config, "CurrentSceneCollectionChanged", eventData);
BroadcastEvent(EventSubscription::Config, "CurrentSceneCollectionChanged", eventData);
}
/**
* The scene collection list has changed.
*
* @dataField sceneCollections | Array<String> | Updated list of scene collections
*
* @eventType SceneCollectionListChanged
* @eventSubscription Config
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api events
*/
void EventHandler::HandleSceneCollectionListChanged()
{
json eventData;
eventData["sceneCollections"] = Utils::Obs::ListHelper::GetSceneCollectionList();
_webSocketServer->BroadcastEvent(EventSubscription::Config, "SceneCollectionListChanged", eventData);
eventData["sceneCollections"] = Utils::Obs::ArrayHelper::GetSceneCollectionList();
BroadcastEvent(EventSubscription::Config, "SceneCollectionListChanged", eventData);
}
/**
* The current profile has begun changing.
*
* @dataField profileName | String | Name of the current profile
*
* @eventType CurrentProfileChanging
* @eventSubscription Config
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api events
*/
void EventHandler::HandleCurrentProfileChanging()
{
json eventData;
eventData["profileName"] = Utils::Obs::StringHelper::GetCurrentProfile();
BroadcastEvent(EventSubscription::Config, "CurrentProfileChanging", eventData);
}
/**
* The current profile has changed.
*
* @dataField profileName | String | Name of the new profile
*
* @eventType CurrentProfileChanged
* @eventSubscription Config
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api events
*/
void EventHandler::HandleCurrentProfileChanged()
{
json eventData;
eventData["profileName"] = Utils::Obs::StringHelper::GetCurrentProfile();
_webSocketServer->BroadcastEvent(EventSubscription::Config, "CurrentProfileChanged", eventData);
BroadcastEvent(EventSubscription::Config, "CurrentProfileChanged", eventData);
}
/**
* The profile list has changed.
*
* @dataField profiles | Array<String> | Updated list of profiles
*
* @eventType ProfileListChanged
* @eventSubscription Config
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api events
*/
void EventHandler::HandleProfileListChanged()
{
json eventData;
eventData["profiles"] = Utils::Obs::ListHelper::GetProfileList();
_webSocketServer->BroadcastEvent(EventSubscription::Config, "ProfileListChanged", eventData);
eventData["profiles"] = Utils::Obs::ArrayHelper::GetProfileList();
BroadcastEvent(EventSubscription::Config, "ProfileListChanged", eventData);
}

View File

@ -1,2 +1,201 @@
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#include "EventHandler.h"
#include "../plugin-macros.generated.h"
void EventHandler::FilterAddMultiHandler(void *param, calldata_t *data)
{
auto eventHandler = static_cast<EventHandler*>(param);
obs_source_t *source = GetCalldataPointer<obs_source_t>(data, "source");
obs_source_t *filter = GetCalldataPointer<obs_source_t>(data, "filter");
if (!(source && filter))
return;
eventHandler->ConnectSourceSignals(filter);
eventHandler->HandleSourceFilterCreated(source, filter);
}
void EventHandler::FilterRemoveMultiHandler(void *param, calldata_t *data)
{
auto eventHandler = static_cast<EventHandler*>(param);
obs_source_t *source = GetCalldataPointer<obs_source_t>(data, "source");
obs_source_t *filter = GetCalldataPointer<obs_source_t>(data, "filter");
if (!(source && filter))
return;
eventHandler->DisconnectSourceSignals(filter);
eventHandler->HandleSourceFilterRemoved(source, filter);
}
/**
* A source's filter list has been reindexed.
*
* @dataField sourceName | String | Name of the source
* @dataField filters | Array<Object> | Array of filter objects
*
* @eventType SourceFilterListReindexed
* @eventSubscription Filters
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category filters
*/
void EventHandler::HandleSourceFilterListReindexed(void *param, calldata_t *data)
{
auto eventHandler = static_cast<EventHandler*>(param);
obs_source_t *source = GetCalldataPointer<obs_source_t>(data, "source");
if (!source)
return;
json eventData;
eventData["sourceName"] = obs_source_get_name(source);
eventData["filters"] = Utils::Obs::ArrayHelper::GetSourceFilterList(source);
eventHandler->BroadcastEvent(EventSubscription::Filters, "SourceFilterListReindexed", eventData);
}
/**
* A filter has been added to a source.
*
* @dataField sourceName | String | Name of the source the filter was added to
* @dataField filterName | String | Name of the filter
* @dataField filterKind | String | The kind of the filter
* @dataField filterIndex | Number | Index position of the filter
* @dataField filterSettings | Object | The settings configured to the filter when it was created
* @dataField defaultFilterSettings | Object | The default settings for the filter
*
* @eventType SourceFilterCreated
* @eventSubscription Filters
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category filters
*/
void EventHandler::HandleSourceFilterCreated(obs_source_t *source, obs_source_t *filter)
{
std::string filterKind = obs_source_get_id(filter);
OBSDataAutoRelease filterSettings = obs_source_get_settings(filter);
OBSDataAutoRelease defaultFilterSettings = obs_get_source_defaults(filterKind.c_str());
json eventData;
eventData["sourceName"] = obs_source_get_name(source);
eventData["filterName"] = obs_source_get_name(filter);
eventData["filterKind"] = filterKind;
eventData["filterIndex"] = Utils::Obs::NumberHelper::GetSourceFilterIndex(source, filter);
eventData["filterSettings"] = Utils::Json::ObsDataToJson(filterSettings);
eventData["defaultFilterSettings"] = Utils::Json::ObsDataToJson(defaultFilterSettings, true);
BroadcastEvent(EventSubscription::Filters, "SourceFilterCreated", eventData);
}
/**
* A filter has been removed from a source.
*
* @dataField sourceName | String | Name of the source the filter was on
* @dataField filterName | String | Name of the filter
*
* @eventType SourceFilterRemoved
* @eventSubscription Filters
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category filters
*/
void EventHandler::HandleSourceFilterRemoved(obs_source_t *source, obs_source_t *filter)
{
json eventData;
eventData["sourceName"] = obs_source_get_name(source);
eventData["filterName"] = obs_source_get_name(filter);
BroadcastEvent(EventSubscription::Filters, "SourceFilterRemoved", eventData);
}
/**
* The name of a source filter has changed.
*
* @dataField sourceName | String | The source the filter is on
* @dataField oldFilterName | String | Old name of the filter
* @dataField filterName | String | New name of the filter
*
* @eventType SourceFilterNameChanged
* @eventSubscription Filters
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category filters
*/
void EventHandler::HandleSourceFilterNameChanged(void *param, calldata_t *data)
{
auto eventHandler = static_cast<EventHandler*>(param);
obs_source_t *filter = GetCalldataPointer<obs_source_t>(data, "source");
if (!filter)
return;
json eventData;
eventData["sourceName"] = obs_source_get_name(obs_filter_get_parent(filter));
eventData["oldFilterName"] = calldata_string(data, "prev_name");
eventData["filterName"] = calldata_string(data, "new_name");
eventHandler->BroadcastEvent(EventSubscription::Filters, "SourceFilterNameChanged", eventData);
}
/**
* A source filter's enable state has changed.
*
* @dataField sourceName | String | Name of the source the filter is on
* @dataField filterName | String | Name of the filter
* @dataField filterEnabled | Boolean | Whether the filter is enabled
*
* @eventType SourceFilterEnableStateChanged
* @eventSubscription Filters
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category filters
*/
void EventHandler::HandleSourceFilterEnableStateChanged(void *param, calldata_t *data)
{
auto eventHandler = static_cast<EventHandler*>(param);
obs_source_t *filter = GetCalldataPointer<obs_source_t>(data, "source");
if (!filter)
return;
// Not OBSSourceAutoRelease as get_parent doesn't increment refcount
obs_source_t *source = obs_filter_get_parent(filter);
if (!source)
return;
bool filterEnabled = calldata_bool(data, "enabled");
json eventData;
eventData["sourceName"] = obs_source_get_name(source);
eventData["filterName"] = obs_source_get_name(filter);
eventData["filterEnabled"] = filterEnabled;
eventHandler->BroadcastEvent(EventSubscription::Filters, "SourceFilterEnableStateChanged", eventData);
}

View File

@ -1,14 +1,36 @@
#include "EventHandler.h"
#include "../plugin-macros.generated.h"
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#include "EventHandler.h"
/**
* OBS has begun the shutdown process.
*
* @eventType ExitStarted
* @eventSubscription General
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category general
* @api events
*/
void EventHandler::HandleExitStarted()
{
_webSocketServer->BroadcastEvent(EventSubscription::General, "ExitStarted");
}
void EventHandler::HandleStudioModeStateChanged(bool enabled)
{
json eventData;
eventData["studioModeEnabled"] = enabled;
_webSocketServer->BroadcastEvent(EventSubscription::General, "StudioModeStateChanged", eventData);
BroadcastEvent(EventSubscription::General, "ExitStarted");
}

View File

@ -1,6 +1,41 @@
#include "EventHandler.h"
#include "../plugin-macros.generated.h"
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#include "EventHandler.h"
/**
* An input has been created.
*
* @dataField inputName | String | Name of the input
* @dataField inputKind | String | The kind of the input
* @dataField unversionedInputKind | String | The unversioned kind of input (aka no `_v2` stuff)
* @dataField inputSettings | Object | The settings configured to the input when it was created
* @dataField defaultInputSettings | Object | The default settings for the input
*
* @eventType InputCreated
* @eventSubscription Inputs
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category inputs
*/
void EventHandler::HandleInputCreated(obs_source_t *source)
{
std::string inputKind = obs_source_get_id(source);
@ -13,27 +48,73 @@ void EventHandler::HandleInputCreated(obs_source_t *source)
eventData["unversionedInputKind"] = obs_source_get_unversioned_id(source);
eventData["inputSettings"] = Utils::Json::ObsDataToJson(inputSettings);
eventData["defaultInputSettings"] = Utils::Json::ObsDataToJson(defaultInputSettings, true);
_webSocketServer->BroadcastEvent(EventSubscription::Inputs, "InputCreated", eventData);
BroadcastEvent(EventSubscription::Inputs, "InputCreated", eventData);
}
/**
* An input has been removed.
*
* @dataField inputName | String | Name of the input
*
* @eventType InputRemoved
* @eventSubscription Inputs
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category inputs
*/
void EventHandler::HandleInputRemoved(obs_source_t *source)
{
json eventData;
eventData["inputName"] = obs_source_get_name(source);
_webSocketServer->BroadcastEvent(EventSubscription::Inputs, "InputRemoved", eventData);
BroadcastEvent(EventSubscription::Inputs, "InputRemoved", eventData);
}
void EventHandler::HandleInputNameChanged(obs_source_t *source, std::string oldInputName, std::string inputName)
/**
* The name of an input has changed.
*
* @dataField oldInputName | String | Old name of the input
* @dataField inputName | String | New name of the input
*
* @eventType InputNameChanged
* @eventSubscription Inputs
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category inputs
*/
void EventHandler::HandleInputNameChanged(obs_source_t *, std::string oldInputName, std::string inputName)
{
json eventData;
eventData["oldInputName"] = oldInputName;
eventData["inputName"] = inputName;
_webSocketServer->BroadcastEvent(EventSubscription::Inputs, "InputNameChanged", eventData);
BroadcastEvent(EventSubscription::Inputs, "InputNameChanged", eventData);
}
/**
* An input's active state has changed.
*
* When an input is active, it means it's being shown by the program feed.
*
* @dataField inputName | String | Name of the input
* @dataField videoActive | Boolean | Whether the input is active
*
* @eventType InputActiveStateChanged
* @eventSubscription InputActiveStateChanged
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category inputs
*/
void EventHandler::HandleInputActiveStateChanged(void *param, calldata_t *data)
{
auto eventHandler = reinterpret_cast<EventHandler*>(param);
auto eventHandler = static_cast<EventHandler*>(param);
if (!eventHandler->_inputActiveStateChangedRef.load())
return;
obs_source_t *source = GetCalldataPointer<obs_source_t>(data, "source");
if (!source)
@ -45,12 +126,31 @@ void EventHandler::HandleInputActiveStateChanged(void *param, calldata_t *data)
json eventData;
eventData["inputName"] = obs_source_get_name(source);
eventData["videoActive"] = obs_source_active(source);
eventHandler->_webSocketServer->BroadcastEvent(EventSubscription::InputActiveStateChanged, "InputActiveStateChanged", eventData);
eventHandler->BroadcastEvent(EventSubscription::InputActiveStateChanged, "InputActiveStateChanged", eventData);
}
/**
* An input's show state has changed.
*
* When an input is showing, it means it's being shown by the preview or a dialog.
*
* @dataField inputName | String | Name of the input
* @dataField videoShowing | Boolean | Whether the input is showing
*
* @eventType InputShowStateChanged
* @eventSubscription InputShowStateChanged
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category inputs
*/
void EventHandler::HandleInputShowStateChanged(void *param, calldata_t *data)
{
auto eventHandler = reinterpret_cast<EventHandler*>(param);
auto eventHandler = static_cast<EventHandler*>(param);
if (!eventHandler->_inputShowStateChangedRef.load())
return;
obs_source_t *source = GetCalldataPointer<obs_source_t>(data, "source");
if (!source)
@ -62,12 +162,26 @@ void EventHandler::HandleInputShowStateChanged(void *param, calldata_t *data)
json eventData;
eventData["inputName"] = obs_source_get_name(source);
eventData["videoShowing"] = obs_source_showing(source);
eventHandler->_webSocketServer->BroadcastEvent(EventSubscription::InputShowStateChanged, "InputShowStateChanged", eventData);
eventHandler->BroadcastEvent(EventSubscription::InputShowStateChanged, "InputShowStateChanged", eventData);
}
/**
* An input's mute state has changed.
*
* @dataField inputName | String | Name of the input
* @dataField inputMuted | Boolean | Whether the input is muted
*
* @eventType InputMuteStateChanged
* @eventSubscription Inputs
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category inputs
*/
void EventHandler::HandleInputMuteStateChanged(void *param, calldata_t *data)
{
auto eventHandler = reinterpret_cast<EventHandler*>(param);
auto eventHandler = static_cast<EventHandler*>(param);
obs_source_t *source = GetCalldataPointer<obs_source_t>(data, "source");
if (!source)
@ -79,12 +193,27 @@ void EventHandler::HandleInputMuteStateChanged(void *param, calldata_t *data)
json eventData;
eventData["inputName"] = obs_source_get_name(source);
eventData["inputMuted"] = obs_source_muted(source);
eventHandler->_webSocketServer->BroadcastEvent(EventSubscription::Inputs, "InputMuteStateChanged", eventData);
eventHandler->BroadcastEvent(EventSubscription::Inputs, "InputMuteStateChanged", eventData);
}
/**
* An input's volume level has changed.
*
* @dataField inputName | String | Name of the input
* @dataField inputVolumeMul | Number | New volume level in multimap
* @dataField inputVolumeDb | Number | New volume level in dB
*
* @eventType InputVolumeChanged
* @eventSubscription Inputs
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category inputs
*/
void EventHandler::HandleInputVolumeChanged(void *param, calldata_t *data)
{
auto eventHandler = reinterpret_cast<EventHandler*>(param);
auto eventHandler = static_cast<EventHandler*>(param);
obs_source_t *source = GetCalldataPointer<obs_source_t>(data, "source");
if (!source)
@ -104,12 +233,59 @@ void EventHandler::HandleInputVolumeChanged(void *param, calldata_t *data)
eventData["inputName"] = obs_source_get_name(source);
eventData["inputVolumeMul"] = inputVolumeMul;
eventData["inputVolumeDb"] = inputVolumeDb;
eventHandler->_webSocketServer->BroadcastEvent(EventSubscription::Inputs, "InputVolumeChanged", eventData);
eventHandler->BroadcastEvent(EventSubscription::Inputs, "InputVolumeChanged", eventData);
}
/**
* The audio balance value of an input has changed.
*
* @dataField inputName | String | Name of the affected input
* @dataField inputAudioBalance | Number | New audio balance value of the input
*
* @eventType InputAudioBalanceChanged
* @eventSubscription Inputs
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @category inputs
* @api events
*/
void EventHandler::HandleInputAudioBalanceChanged(void *param, calldata_t *data)
{
auto eventHandler = static_cast<EventHandler*>(param);
obs_source_t *source = GetCalldataPointer<obs_source_t>(data, "source");
if (!source)
return;
if (obs_source_get_type(source) != OBS_SOURCE_TYPE_INPUT)
return;
float inputAudioBalance = (float)calldata_float(data, "balance");
json eventData;
eventData["inputName"] = obs_source_get_name(source);
eventData["inputAudioBalance"] = inputAudioBalance;
eventHandler->BroadcastEvent(EventSubscription::Inputs, "InputAudioBalanceChanged", eventData);
}
/**
* The sync offset of an input has changed.
*
* @dataField inputName | String | Name of the input
* @dataField inputAudioSyncOffset | Number | New sync offset in milliseconds
*
* @eventType InputAudioSyncOffsetChanged
* @eventSubscription Inputs
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category inputs
*/
void EventHandler::HandleInputAudioSyncOffsetChanged(void *param, calldata_t *data)
{
auto eventHandler = reinterpret_cast<EventHandler*>(param);
auto eventHandler = static_cast<EventHandler*>(param);
obs_source_t *source = GetCalldataPointer<obs_source_t>(data, "source");
if (!source)
@ -123,12 +299,26 @@ void EventHandler::HandleInputAudioSyncOffsetChanged(void *param, calldata_t *da
json eventData;
eventData["inputName"] = obs_source_get_name(source);
eventData["inputAudioSyncOffset"] = inputAudioSyncOffset / 1000000;
eventHandler->_webSocketServer->BroadcastEvent(EventSubscription::Inputs, "InputAudioSyncOffsetChanged", eventData);
eventHandler->BroadcastEvent(EventSubscription::Inputs, "InputAudioSyncOffsetChanged", eventData);
}
/**
* The audio tracks of an input have changed.
*
* @dataField inputName | String | Name of the input
* @dataField inputAudioTracks | Object | Object of audio tracks along with their associated enable states
*
* @eventType InputAudioTracksChanged
* @eventSubscription Inputs
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category inputs
*/
void EventHandler::HandleInputAudioTracksChanged(void *param, calldata_t *data)
{
auto eventHandler = reinterpret_cast<EventHandler*>(param);
auto eventHandler = static_cast<EventHandler*>(param);
obs_source_t *source = GetCalldataPointer<obs_source_t>(data, "source");
if (!source)
@ -140,12 +330,72 @@ void EventHandler::HandleInputAudioTracksChanged(void *param, calldata_t *data)
long long tracks = calldata_int(data, "mixers");
json inputAudioTracks;
for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) {
inputAudioTracks[std::to_string(i + 1)] = (bool)((1 << i) & tracks);
for (long long i = 0; i < MAX_AUDIO_MIXES; i++) {
inputAudioTracks[std::to_string(i + 1)] = (bool)((tracks >> i) & 1);
}
json eventData;
eventData["inputName"] = obs_source_get_name(source);
eventData["inputAudioTracks"] = inputAudioTracks;
eventHandler->_webSocketServer->BroadcastEvent(EventSubscription::Inputs, "InputAudioTracksChanged", eventData);
eventHandler->BroadcastEvent(EventSubscription::Inputs, "InputAudioTracksChanged", eventData);
}
/**
* The monitor type of an input has changed.
*
* Available types are:
* - `OBS_MONITORING_TYPE_NONE`
* - `OBS_MONITORING_TYPE_MONITOR_ONLY`
* - `OBS_MONITORING_TYPE_MONITOR_AND_OUTPUT`
*
* @dataField inputName | String | Name of the input
* @dataField monitorType | String | New monitor type of the input
*
* @eventType InputAudioMonitorTypeChanged
* @eventSubscription Inputs
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category inputs
*/
void EventHandler::HandleInputAudioMonitorTypeChanged(void *param, calldata_t *data)
{
auto eventHandler = static_cast<EventHandler*>(param);
obs_source_t *source = GetCalldataPointer<obs_source_t>(data, "source");
if (!source)
return;
if (obs_source_get_type(source) != OBS_SOURCE_TYPE_INPUT)
return;
enum obs_monitoring_type monitorType = (obs_monitoring_type)calldata_int(data, "type");
std::string monitorTypeString = Utils::Obs::StringHelper::GetInputMonitorType(monitorType);
json eventData;
eventData["inputName"] = obs_source_get_name(source);
eventData["monitorType"] = monitorTypeString;
eventHandler->BroadcastEvent(EventSubscription::Inputs, "InputAudioMonitorTypeChanged", eventData);
}
/**
* A high-volume event providing volume levels of all active inputs every 50 milliseconds.
*
* @dataField inputs | Array<Object> | Array of active inputs with their associated volume levels
*
* @eventType InputVolumeMeters
* @eventSubscription InputVolumeMeters
* @complexity 4
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category inputs
*/
void EventHandler::HandleInputVolumeMeters(std::vector<json> inputs)
{
json eventData;
eventData["inputs"] = inputs;
BroadcastEvent(EventSubscription::InputVolumeMeters, "InputVolumeMeters", eventData);
}

View File

@ -1,5 +1,23 @@
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#include "EventHandler.h"
#include "../plugin-macros.generated.h"
#define CASE(x) case x: return #x;
@ -17,7 +35,7 @@ std::string GetMediaInputActionString(ObsMediaInputAction action) {
void EventHandler::SourceMediaPauseMultiHandler(void *param, calldata_t *data)
{
auto eventHandler = reinterpret_cast<EventHandler*>(param);
auto eventHandler = static_cast<EventHandler*>(param);
obs_source_t *source = GetCalldataPointer<obs_source_t>(data, "source");
if (!source)
@ -31,7 +49,7 @@ void EventHandler::SourceMediaPauseMultiHandler(void *param, calldata_t *data)
void EventHandler::SourceMediaPlayMultiHandler(void *param, calldata_t *data)
{
auto eventHandler = reinterpret_cast<EventHandler*>(param);
auto eventHandler = static_cast<EventHandler*>(param);
obs_source_t *source = GetCalldataPointer<obs_source_t>(data, "source");
if (!source)
@ -45,7 +63,7 @@ void EventHandler::SourceMediaPlayMultiHandler(void *param, calldata_t *data)
void EventHandler::SourceMediaRestartMultiHandler(void *param, calldata_t *data)
{
auto eventHandler = reinterpret_cast<EventHandler*>(param);
auto eventHandler = static_cast<EventHandler*>(param);
obs_source_t *source = GetCalldataPointer<obs_source_t>(data, "source");
if (!source)
@ -59,7 +77,7 @@ void EventHandler::SourceMediaRestartMultiHandler(void *param, calldata_t *data)
void EventHandler::SourceMediaStopMultiHandler(void *param, calldata_t *data)
{
auto eventHandler = reinterpret_cast<EventHandler*>(param);
auto eventHandler = static_cast<EventHandler*>(param);
obs_source_t *source = GetCalldataPointer<obs_source_t>(data, "source");
if (!source)
@ -73,7 +91,7 @@ void EventHandler::SourceMediaStopMultiHandler(void *param, calldata_t *data)
void EventHandler::SourceMediaNextMultiHandler(void *param, calldata_t *data)
{
auto eventHandler = reinterpret_cast<EventHandler*>(param);
auto eventHandler = static_cast<EventHandler*>(param);
obs_source_t *source = GetCalldataPointer<obs_source_t>(data, "source");
if (!source)
@ -87,7 +105,7 @@ void EventHandler::SourceMediaNextMultiHandler(void *param, calldata_t *data)
void EventHandler::SourceMediaPreviousMultiHandler(void *param, calldata_t *data)
{
auto eventHandler = reinterpret_cast<EventHandler*>(param);
auto eventHandler = static_cast<EventHandler*>(param);
obs_source_t *source = GetCalldataPointer<obs_source_t>(data, "source");
if (!source)
@ -99,9 +117,22 @@ void EventHandler::SourceMediaPreviousMultiHandler(void *param, calldata_t *data
eventHandler->HandleMediaInputActionTriggered(source, OBS_WEBSOCKET_MEDIA_INPUT_ACTION_PREVIOUS);
}
/**
* A media input has started playing.
*
* @dataField inputName | String | Name of the input
*
* @eventType MediaInputPlaybackStarted
* @eventSubscription MediaInputs
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category media inputs
*/
void EventHandler::HandleMediaInputPlaybackStarted(void *param, calldata_t *data)
{
auto eventHandler = reinterpret_cast<EventHandler*>(param);
auto eventHandler = static_cast<EventHandler*>(param);
obs_source_t *source = GetCalldataPointer<obs_source_t>(data, "source");
if (!source)
@ -112,12 +143,25 @@ void EventHandler::HandleMediaInputPlaybackStarted(void *param, calldata_t *data
json eventData;
eventData["inputName"] = obs_source_get_name(source);
eventHandler->_webSocketServer->BroadcastEvent(EventSubscription::MediaInputs, "MediaInputPlaybackStarted", eventData);
eventHandler->BroadcastEvent(EventSubscription::MediaInputs, "MediaInputPlaybackStarted", eventData);
}
/**
* A media input has finished playing.
*
* @dataField inputName | String | Name of the input
*
* @eventType MediaInputPlaybackEnded
* @eventSubscription MediaInputs
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category media inputs
*/
void EventHandler::HandleMediaInputPlaybackEnded(void *param, calldata_t *data)
{
auto eventHandler = reinterpret_cast<EventHandler*>(param);
auto eventHandler = static_cast<EventHandler*>(param);
obs_source_t *source = GetCalldataPointer<obs_source_t>(data, "source");
if (!source)
@ -128,13 +172,27 @@ void EventHandler::HandleMediaInputPlaybackEnded(void *param, calldata_t *data)
json eventData;
eventData["inputName"] = obs_source_get_name(source);
eventHandler->_webSocketServer->BroadcastEvent(EventSubscription::MediaInputs, "MediaInputPlaybackEnded", eventData);
eventHandler->BroadcastEvent(EventSubscription::MediaInputs, "MediaInputPlaybackEnded", eventData);
}
/**
* An action has been performed on an input.
*
* @dataField inputName | String | Name of the input
* @dataField mediaAction | String | Action performed on the input. See `ObsMediaInputAction` enum
*
* @eventType MediaInputActionTriggered
* @eventSubscription MediaInputs
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category media inputs
*/
void EventHandler::HandleMediaInputActionTriggered(obs_source_t *source, ObsMediaInputAction action)
{
json eventData;
eventData["inputName"] = obs_source_get_name(source);
eventData["mediaAction"] = GetMediaInputActionString(action);
_webSocketServer->BroadcastEvent(EventSubscription::MediaInputs, "MediaInputActionTriggered", eventData);
BroadcastEvent(EventSubscription::MediaInputs, "MediaInputActionTriggered", eventData);
}

View File

@ -1,21 +1,25 @@
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#include "EventHandler.h"
#include "../plugin-macros.generated.h"
#define CASE(x) case x: return #x;
std::string GetOutputStateString(ObsOutputState state) {
switch (state) {
default:
CASE(OBS_WEBSOCKET_OUTPUT_STARTING)
CASE(OBS_WEBSOCKET_OUTPUT_STARTED)
CASE(OBS_WEBSOCKET_OUTPUT_STOPPING)
CASE(OBS_WEBSOCKET_OUTPUT_STOPPED)
CASE(OBS_WEBSOCKET_OUTPUT_PAUSED)
CASE(OBS_WEBSOCKET_OUTPUT_RESUMED)
}
}
bool GetOutputStateActive(ObsOutputState state) {
static bool GetOutputStateActive(ObsOutputState state) {
switch(state) {
case OBS_WEBSOCKET_OUTPUT_STARTED:
case OBS_WEBSOCKET_OUTPUT_RESUMED:
@ -30,41 +34,110 @@ bool GetOutputStateActive(ObsOutputState state) {
}
}
/**
* The state of the stream output has changed.
*
* @dataField outputActive | Boolean | Whether the output is active
* @dataField outputState | String | The specific state of the output
*
* @eventType StreamStateChanged
* @eventSubscription Outputs
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category outputs
*/
void EventHandler::HandleStreamStateChanged(ObsOutputState state)
{
json eventData;
eventData["outputActive"] = GetOutputStateActive(state);
eventData["outputState"] = GetOutputStateString(state);
_webSocketServer->BroadcastEvent(EventSubscription::Outputs, "StreamStateChanged", eventData);
eventData["outputState"] = Utils::Obs::StringHelper::GetOutputState(state);
BroadcastEvent(EventSubscription::Outputs, "StreamStateChanged", eventData);
}
/**
* The state of the record output has changed.
*
* @dataField outputActive | Boolean | Whether the output is active
* @dataField outputState | String | The specific state of the output
*
* @eventType RecordStateChanged
* @eventSubscription Outputs
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category outputs
*/
void EventHandler::HandleRecordStateChanged(ObsOutputState state)
{
json eventData;
eventData["outputActive"] = GetOutputStateActive(state);
eventData["outputState"] = GetOutputStateString(state);
_webSocketServer->BroadcastEvent(EventSubscription::Outputs, "RecordStateChanged", eventData);
eventData["outputState"] = Utils::Obs::StringHelper::GetOutputState(state);
BroadcastEvent(EventSubscription::Outputs, "RecordStateChanged", eventData);
}
/**
* The state of the replay buffer output has changed.
*
* @dataField outputActive | Boolean | Whether the output is active
* @dataField outputState | String | The specific state of the output
*
* @eventType ReplayBufferStateChanged
* @eventSubscription Outputs
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category outputs
*/
void EventHandler::HandleReplayBufferStateChanged(ObsOutputState state)
{
json eventData;
eventData["outputActive"] = GetOutputStateActive(state);
eventData["outputState"] = GetOutputStateString(state);
_webSocketServer->BroadcastEvent(EventSubscription::Outputs, "ReplayBufferStateChanged", eventData);
eventData["outputState"] = Utils::Obs::StringHelper::GetOutputState(state);
BroadcastEvent(EventSubscription::Outputs, "ReplayBufferStateChanged", eventData);
}
/**
* The state of the virtualcam output has changed.
*
* @dataField outputActive | Boolean | Whether the output is active
* @dataField outputState | String | The specific state of the output
*
* @eventType VirtualcamStateChanged
* @eventSubscription Outputs
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category outputs
*/
void EventHandler::HandleVirtualcamStateChanged(ObsOutputState state)
{
json eventData;
eventData["outputActive"] = GetOutputStateActive(state);
eventData["outputState"] = GetOutputStateString(state);
_webSocketServer->BroadcastEvent(EventSubscription::Outputs, "VirtualcamStateChanged", eventData);
eventData["outputState"] = Utils::Obs::StringHelper::GetOutputState(state);
BroadcastEvent(EventSubscription::Outputs, "VirtualcamStateChanged", eventData);
}
/**
* The replay buffer has been saved.
*
* @dataField savedReplayPath | String | Path of the saved replay file
*
* @eventType ReplayBufferSaved
* @eventSubscription Outputs
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category outputs
*/
void EventHandler::HandleReplayBufferSaved()
{
json eventData;
eventData["savedReplayPath"] = Utils::Obs::StringHelper::GetLastReplayBufferFilePath();
_webSocketServer->BroadcastEvent(EventSubscription::Outputs, "ReplayBufferSaved", eventData);
BroadcastEvent(EventSubscription::Outputs, "ReplayBufferSaved", eventData);
}

View File

@ -1,9 +1,43 @@
#include "EventHandler.h"
#include "../plugin-macros.generated.h"
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#include "EventHandler.h"
/**
* A scene item has been created.
*
* @dataField sceneName | String | Name of the scene the item was added to
* @dataField sourceName | String | Name of the underlying source (input/scene)
* @dataField sceneItemId | Number | Numeric ID of the scene item
* @dataField sceneItemIndex | Number | Index position of the item
*
* @eventType SceneItemCreated
* @eventSubscription SceneItems
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category scene items
*/
void EventHandler::HandleSceneItemCreated(void *param, calldata_t *data)
{
auto eventHandler = reinterpret_cast<EventHandler*>(param);
auto eventHandler = static_cast<EventHandler*>(param);
obs_scene_t *scene = GetCalldataPointer<obs_scene_t>(data, "scene");
if (!scene)
@ -15,16 +49,32 @@ void EventHandler::HandleSceneItemCreated(void *param, calldata_t *data)
json eventData;
eventData["sceneName"] = obs_source_get_name(obs_scene_get_source(scene));
eventData["inputName"] = obs_source_get_name(obs_sceneitem_get_source(sceneItem));
eventData["sourceName"] = obs_source_get_name(obs_sceneitem_get_source(sceneItem));
eventData["sceneItemId"] = obs_sceneitem_get_id(sceneItem);
eventData["sceneItemIndex"] = obs_sceneitem_get_order_position(sceneItem);
eventHandler->_webSocketServer->BroadcastEvent(EventSubscription::SceneItems, "SceneItemCreated", eventData);
eventHandler->BroadcastEvent(EventSubscription::SceneItems, "SceneItemCreated", eventData);
}
// Will not be emitted if an item is removed due to the parent scene being removed.
/**
* A scene item has been removed.
*
* This event is not emitted when the scene the item is in is removed.
*
* @dataField sceneName | String | Name of the scene the item was removed from
* @dataField sourceName | String | Name of the underlying source (input/scene)
* @dataField sceneItemId | Number | Numeric ID of the scene item
*
* @eventType SceneItemRemoved
* @eventSubscription SceneItems
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category scene items
*/
void EventHandler::HandleSceneItemRemoved(void *param, calldata_t *data)
{
auto eventHandler = reinterpret_cast<EventHandler*>(param);
auto eventHandler = static_cast<EventHandler*>(param);
obs_scene_t *scene = GetCalldataPointer<obs_scene_t>(data, "scene");
if (!scene)
@ -36,15 +86,28 @@ void EventHandler::HandleSceneItemRemoved(void *param, calldata_t *data)
json eventData;
eventData["sceneName"] = obs_source_get_name(obs_scene_get_source(scene));
eventData["inputName"] = obs_source_get_name(obs_sceneitem_get_source(sceneItem));
eventData["sourceName"] = obs_source_get_name(obs_sceneitem_get_source(sceneItem));
eventData["sceneItemId"] = obs_sceneitem_get_id(sceneItem);
eventData["sceneItemIndex"] = obs_sceneitem_get_order_position(sceneItem);
eventHandler->_webSocketServer->BroadcastEvent(EventSubscription::SceneItems, "SceneItemRemoved", eventData);
eventHandler->BroadcastEvent(EventSubscription::SceneItems, "SceneItemRemoved", eventData);
}
/**
* A scene's item list has been reindexed.
*
* @dataField sceneName | String | Name of the scene
* @dataField sceneItems | Array<Object> | Array of scene item objects
*
* @eventType SceneItemListReindexed
* @eventSubscription SceneItems
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category scene items
*/
void EventHandler::HandleSceneItemListReindexed(void *param, calldata_t *data)
{
auto eventHandler = reinterpret_cast<EventHandler*>(param);
auto eventHandler = static_cast<EventHandler*>(param);
obs_scene_t *scene = GetCalldataPointer<obs_scene_t>(data, "scene");
if (!scene)
@ -52,13 +115,28 @@ void EventHandler::HandleSceneItemListReindexed(void *param, calldata_t *data)
json eventData;
eventData["sceneName"] = obs_source_get_name(obs_scene_get_source(scene));
eventData["sceneItems"] = Utils::Obs::ListHelper::GetSceneItemList(scene, true);
eventHandler->_webSocketServer->BroadcastEvent(EventSubscription::SceneItems, "SceneItemReindexed", eventData);
eventData["sceneItems"] = Utils::Obs::ArrayHelper::GetSceneItemList(scene, true);
eventHandler->BroadcastEvent(EventSubscription::SceneItems, "SceneItemListReindexed", eventData);
}
/**
* A scene item's enable state has changed.
*
* @dataField sceneName | String | Name of the scene the item is in
* @dataField sceneItemId | Number | Numeric ID of the scene item
* @dataField sceneItemEnabled | Boolean | Whether the scene item is enabled (visible)
*
* @eventType SceneItemEnableStateChanged
* @eventSubscription SceneItems
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category scene items
*/
void EventHandler::HandleSceneItemEnableStateChanged(void *param, calldata_t *data)
{
auto eventHandler = reinterpret_cast<EventHandler*>(param);
auto eventHandler = static_cast<EventHandler*>(param);
obs_scene_t *scene = GetCalldataPointer<obs_scene_t>(data, "scene");
if (!scene)
@ -74,12 +152,27 @@ void EventHandler::HandleSceneItemEnableStateChanged(void *param, calldata_t *da
eventData["sceneName"] = obs_source_get_name(obs_scene_get_source(scene));
eventData["sceneItemId"] = obs_sceneitem_get_id(sceneItem);
eventData["sceneItemEnabled"] = sceneItemEnabled;
eventHandler->_webSocketServer->BroadcastEvent(EventSubscription::SceneItems, "SceneItemEnableStateChanged", eventData);
eventHandler->BroadcastEvent(EventSubscription::SceneItems, "SceneItemEnableStateChanged", eventData);
}
/**
* A scene item's lock state has changed.
*
* @dataField sceneName | String | Name of the scene the item is in
* @dataField sceneItemId | Number | Numeric ID of the scene item
* @dataField sceneItemLocked | Boolean | Whether the scene item is locked
*
* @eventType SceneItemLockStateChanged
* @eventSubscription SceneItems
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category scene items
*/
void EventHandler::HandleSceneItemLockStateChanged(void *param, calldata_t *data)
{
auto eventHandler = reinterpret_cast<EventHandler*>(param);
auto eventHandler = static_cast<EventHandler*>(param);
obs_scene_t *scene = GetCalldataPointer<obs_scene_t>(data, "scene");
if (!scene)
@ -95,13 +188,74 @@ void EventHandler::HandleSceneItemLockStateChanged(void *param, calldata_t *data
eventData["sceneName"] = obs_source_get_name(obs_scene_get_source(scene));
eventData["sceneItemId"] = obs_sceneitem_get_id(sceneItem);
eventData["sceneItemLocked"] = sceneItemLocked;
eventHandler->_webSocketServer->BroadcastEvent(EventSubscription::SceneItems, "SceneItemLockStateChanged", eventData);
eventHandler->BroadcastEvent(EventSubscription::SceneItems, "SceneItemLockStateChanged", eventData);
}
void EventHandler::HandleSceneItemTransformChanged(void *param, calldata_t *data)
/**
* A scene item has been selected in the Ui.
*
* @dataField sceneName | String | Name of the scene the item is in
* @dataField sceneItemId | Number | Numeric ID of the scene item
*
* @eventType SceneItemSelected
* @eventSubscription SceneItems
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category scene items
*/
void EventHandler::HandleSceneItemSelected(void *param, calldata_t *data)
{
auto eventHandler = reinterpret_cast<EventHandler*>(param);
auto eventHandler = static_cast<EventHandler*>(param);
obs_scene_t *scene = GetCalldataPointer<obs_scene_t>(data, "scene");
if (!scene)
return;
obs_sceneitem_t *sceneItem = GetCalldataPointer<obs_sceneitem_t>(data, "item");
if (!sceneItem)
return;
json eventData;
eventHandler->_webSocketServer->BroadcastEvent(EventSubscription::SceneItems, "SceneItemTransformChanged", eventData);
eventData["sceneName"] = obs_source_get_name(obs_scene_get_source(scene));
eventData["sceneItemId"] = obs_sceneitem_get_id(sceneItem);
eventHandler->BroadcastEvent(EventSubscription::SceneItems, "SceneItemSelected", eventData);
}
/**
* The transform/crop of a scene item has changed.
*
* @dataField sceneName | String | The name of the scene the item is in
* @dataField sceneItemId | Number | Numeric ID of the scene item
* @dataField sceneItemTransform | Object | New transform/crop info of the scene item
*
* @eventType SceneItemTransformChanged
* @eventSubscription SceneItemTransformChanged
* @complexity 4
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category scene items
*/
void EventHandler::HandleSceneItemTransformChanged(void *param, calldata_t *data)
{
auto eventHandler = static_cast<EventHandler*>(param);
if (!eventHandler->_sceneItemTransformChangedRef.load())
return;
obs_scene_t *scene = GetCalldataPointer<obs_scene_t>(data, "scene");
if (!scene)
return;
obs_sceneitem_t *sceneItem = GetCalldataPointer<obs_sceneitem_t>(data, "item");
if (!sceneItem)
return;
json eventData;
eventData["sceneName"] = obs_source_get_name(obs_scene_get_source(scene));
eventData["sceneItemId"] = obs_sceneitem_get_id(sceneItem);
eventData["sceneItemTransform"] = Utils::Obs::ObjectHelper::GetSceneItemTransform(sceneItem);
eventHandler->BroadcastEvent(EventSubscription::SceneItemTransformChanged, "SceneItemTransformChanged", eventData);
}

View File

@ -1,39 +1,125 @@
#include "EventHandler.h"
#include "../plugin-macros.generated.h"
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#include "EventHandler.h"
/**
* A new scene has been created.
*
* @dataField sceneName | String | Name of the new scene
* @dataField isGroup | Boolean | Whether the new scene is a group
*
* @eventType SceneCreated
* @eventSubscription Scenes
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category scenes
*/
void EventHandler::HandleSceneCreated(obs_source_t *source)
{
json eventData;
eventData["sceneName"] = obs_source_get_name(source);
eventData["isGroup"] = obs_source_is_group(source);
_webSocketServer->BroadcastEvent(EventSubscription::Scenes, "SceneCreated", eventData);
BroadcastEvent(EventSubscription::Scenes, "SceneCreated", eventData);
}
/**
* A scene has been removed.
*
* @dataField sceneName | String | Name of the removed scene
* @dataField isGroup | Boolean | Whether the scene was a group
*
* @eventType SceneRemoved
* @eventSubscription Scenes
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category scenes
*/
void EventHandler::HandleSceneRemoved(obs_source_t *source)
{
json eventData;
eventData["sceneName"] = obs_source_get_name(source);
eventData["isGroup"] = obs_source_is_group(source);
_webSocketServer->BroadcastEvent(EventSubscription::Scenes, "SceneRemoved", eventData);
BroadcastEvent(EventSubscription::Scenes, "SceneRemoved", eventData);
}
void EventHandler::HandleSceneNameChanged(obs_source_t *source, std::string oldSceneName, std::string sceneName)
/**
* The name of a scene has changed.
*
* @dataField oldSceneName | String | Old name of the scene
* @dataField sceneName | String | New name of the scene
*
* @eventType SceneNameChanged
* @eventSubscription Scenes
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category scenes
*/
void EventHandler::HandleSceneNameChanged(obs_source_t *, std::string oldSceneName, std::string sceneName)
{
json eventData;
eventData["oldSceneName"] = oldSceneName;
eventData["sceneName"] = sceneName;
_webSocketServer->BroadcastEvent(EventSubscription::Scenes, "SceneNameChanged", eventData);
BroadcastEvent(EventSubscription::Scenes, "SceneNameChanged", eventData);
}
void EventHandler::HandleCurrentSceneChanged()
/**
* The current program scene has changed.
*
* @dataField sceneName | String | Name of the scene that was switched to
*
* @eventType CurrentProgramSceneChanged
* @eventSubscription Scenes
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category scenes
*/
void EventHandler::HandleCurrentProgramSceneChanged()
{
OBSSourceAutoRelease currentScene = obs_frontend_get_current_scene();
json eventData;
eventData["sceneName"] = obs_source_get_name(currentScene);
_webSocketServer->BroadcastEvent(EventSubscription::Scenes, "CurrentSceneChanged", eventData);
BroadcastEvent(EventSubscription::Scenes, "CurrentProgramSceneChanged", eventData);
}
/**
* The current preview scene has changed.
*
* @dataField sceneName | String | Name of the scene that was switched to
*
* @eventType CurrentPreviewSceneChanged
* @eventSubscription Scenes
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category scenes
*/
void EventHandler::HandleCurrentPreviewSceneChanged()
{
OBSSourceAutoRelease currentPreviewScene = obs_frontend_get_current_preview_scene();
@ -44,12 +130,27 @@ void EventHandler::HandleCurrentPreviewSceneChanged()
json eventData;
eventData["sceneName"] = obs_source_get_name(currentPreviewScene);
_webSocketServer->BroadcastEvent(EventSubscription::Scenes, "CurrentPreviewSceneChanged", eventData);
BroadcastEvent(EventSubscription::Scenes, "CurrentPreviewSceneChanged", eventData);
}
/**
* The list of scenes has changed.
*
* TODO: Make OBS fire this event when scenes are reordered.
*
* @dataField scenes | Array<Object> | Updated array of scenes
*
* @eventType SceneListChanged
* @eventSubscription Scenes
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category scenes
*/
void EventHandler::HandleSceneListChanged()
{
json eventData;
eventData["scenes"] = Utils::Obs::ListHelper::GetSceneList();
_webSocketServer->BroadcastEvent(EventSubscription::Scenes, "SceneListChanged", eventData);
eventData["scenes"] = Utils::Obs::ArrayHelper::GetSceneList();
BroadcastEvent(EventSubscription::Scenes, "SceneListChanged", eventData);
}

View File

@ -1,26 +1,147 @@
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#include "EventHandler.h"
#include "../plugin-macros.generated.h"
void EventHandler::HandleTransitionCreated(obs_source_t *source)
/**
* The current scene transition has changed.
*
* @dataField transitionName | String | Name of the new transition
*
* @eventType CurrentSceneTransitionChanged
* @eventSubscription Transitions
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category transitions
*/
void EventHandler::HandleCurrentSceneTransitionChanged()
{
OBSSourceAutoRelease transition = obs_frontend_get_current_transition();
json eventData;
eventData["transitionName"] = obs_source_get_name(transition);
BroadcastEvent(EventSubscription::Transitions, "CurrentSceneTransitionChanged", eventData);
}
/**
* The current scene transition duration has changed.
*
* @dataField transitionDuration | Number | Transition duration in milliseconds
*
* @eventType CurrentSceneTransitionDurationChanged
* @eventSubscription Transitions
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category transitions
*/
void EventHandler::HandleCurrentSceneTransitionDurationChanged()
{
json eventData;
eventData["transitionDuration"] = obs_frontend_get_transition_duration();
BroadcastEvent(EventSubscription::Transitions, "CurrentSceneTransitionDurationChanged", eventData);
}
/**
* A scene transition has started.
*
* @dataField transitionName | String | Scene transition name
*
* @eventType SceneTransitionStarted
* @eventSubscription Transitions
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category transitions
*/
void EventHandler::HandleSceneTransitionStarted(void *param, calldata_t *data)
{
auto eventHandler = static_cast<EventHandler*>(param);
obs_source_t *source = GetCalldataPointer<obs_source_t>(data, "source");
if (!source)
return;
json eventData;
eventData["transitionName"] = obs_source_get_name(source);
eventData["transitionKind"] = obs_source_get_id(source);
eventData["transitionFixed"] = obs_transition_fixed(source);
_webSocketServer->BroadcastEvent(EventSubscription::Transitions, "TransitionCreated", eventData);
eventHandler->BroadcastEvent(EventSubscription::Transitions, "SceneTransitionStarted", eventData);
}
void EventHandler::HandleTransitionRemoved(obs_source_t *source)
/**
* A scene transition has completed fully.
*
* Note: Does not appear to trigger when the transition is interrupted by the user.
*
* @dataField transitionName | String | Scene transition name
*
* @eventType SceneTransitionEnded
* @eventSubscription Transitions
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category transitions
*/
void EventHandler::HandleSceneTransitionEnded(void *param, calldata_t *data)
{
auto eventHandler = static_cast<EventHandler*>(param);
obs_source_t *source = GetCalldataPointer<obs_source_t>(data, "source");
if (!source)
return;
json eventData;
eventData["transitionName"] = obs_source_get_name(source);
_webSocketServer->BroadcastEvent(EventSubscription::Transitions, "TransitionRemoved", eventData);
eventHandler->BroadcastEvent(EventSubscription::Transitions, "SceneTransitionEnded", eventData);
}
void EventHandler::HandleTransitionNameChanged(obs_source_t *source, std::string oldTransitionName, std::string transitionName)
/**
* A scene transition's video has completed fully.
*
* Useful for stinger transitions to tell when the video *actually* ends.
* `SceneTransitionEnded` only signifies the cut point, not the completion of transition playback.
*
* Note: Appears to be called by every transition, regardless of relevance.
*
* @dataField transitionName | String | Scene transition name
*
* @eventType SceneTransitionVideoEnded
* @eventSubscription Transitions
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category transitions
*/
void EventHandler::HandleSceneTransitionVideoEnded(void *param, calldata_t *data)
{
auto eventHandler = static_cast<EventHandler*>(param);
obs_source_t *source = GetCalldataPointer<obs_source_t>(data, "source");
if (!source)
return;
json eventData;
eventData["oldTransitionName"] = oldTransitionName;
eventData["transitionName"] = transitionName;
_webSocketServer->BroadcastEvent(EventSubscription::Transitions, "TransitionNameChanged", eventData);
eventData["transitionName"] = obs_source_get_name(source);
eventHandler->BroadcastEvent(EventSubscription::Transitions, "SceneTransitionVideoEnded", eventData);
}

View File

@ -0,0 +1,40 @@
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#include "EventHandler.h"
/**
* Studio mode has been enabled or disabled.
*
* @dataField studioModeEnabled | Boolean | True == Enabled, False == Disabled
*
* @eventType StudioModeStateChanged
* @eventSubscription Ui
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category ui
* @api events
*/
void EventHandler::HandleStudioModeStateChanged(bool enabled)
{
json eventData;
eventData["studioModeEnabled"] = enabled;
BroadcastEvent(EventSubscription::Ui, "StudioModeStateChanged", eventData);
}

View File

@ -1,34 +1,212 @@
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#pragma once
namespace EventSubscription {
enum EventSubscription {
// Set subscriptions to 0 to disable all events
/**
* Subcription value used to disable all events.
*
* @enumIdentifier None
* @enumValue 0
* @enumType EventSubscription
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
None = 0,
// Receive events in the `General` category
/**
* Subscription value to receive events in the `General` category.
*
* @enumIdentifier General
* @enumValue (1 << 0)
* @enumType EventSubscription
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
General = (1 << 0),
// Receive events in the `Config` category
/**
* Subscription value to receive events in the `Config` category.
*
* @enumIdentifier Config
* @enumValue (1 << 1)
* @enumType EventSubscription
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
Config = (1 << 1),
// Receive events in the `Scenes` category
/**
* Subscription value to receive events in the `Scenes` category.
*
* @enumIdentifier Scenes
* @enumValue (1 << 2)
* @enumType EventSubscription
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
Scenes = (1 << 2),
// Receive events in the `Inputs` category
/**
* Subscription value to receive events in the `Inputs` category.
*
* @enumIdentifier Inputs
* @enumValue (1 << 3)
* @enumType EventSubscription
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
Inputs = (1 << 3),
// Receive events in the `Transitions` category
/**
* Subscription value to receive events in the `Transitions` category.
*
* @enumIdentifier Transitions
* @enumValue (1 << 4)
* @enumType EventSubscription
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
Transitions = (1 << 4),
// Receive events in the `Filters` category
/**
* Subscription value to receive events in the `Filters` category.
*
* @enumIdentifier Filters
* @enumValue (1 << 5)
* @enumType EventSubscription
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
Filters = (1 << 5),
// Receive events in the `Outputs` category
/**
* Subscription value to receive events in the `Outputs` category.
*
* @enumIdentifier Outputs
* @enumValue (1 << 6)
* @enumType EventSubscription
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
Outputs = (1 << 6),
// Receive events in the `Scene Items` category
/**
* Subscription value to receive events in the `SceneItems` category.
*
* @enumIdentifier SceneItems
* @enumValue (1 << 7)
* @enumType EventSubscription
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
SceneItems = (1 << 7),
// Receive events in the `MediaInputs` category
/**
* Subscription value to receive events in the `MediaInputs` category.
*
* @enumIdentifier MediaInputs
* @enumValue (1 << 8)
* @enumType EventSubscription
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
MediaInputs = (1 << 8),
// Receive all event categories
All = (General | Config | Scenes | Inputs | Transitions | Filters | Outputs | SceneItems | MediaInputs),
// InputVolumeMeters event (high-volume)
InputVolumeMeters = (1 << 9),
// InputActiveStateChanged event (high-volume)
InputActiveStateChanged = (1 << 10),
// InputShowStateChanged event (high-volume)
InputShowStateChanged = (1 << 11),
/**
* Subscription value to receive the `VendorEvent` event.
*
* @enumIdentifier Vendors
* @enumValue (1 << 9)
* @enumType EventSubscription
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
Vendors = (1 << 9),
/**
* Subscription value to receive events in the `Ui` category.
*
* @enumIdentifier Ui
* @enumValue (1 << 10)
* @enumType EventSubscription
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
Ui = (1 << 10),
/**
* Helper to receive all non-high-volume events.
*
* @enumIdentifier All
* @enumValue (General | Config | Scenes | Inputs | Transitions | Filters | Outputs | SceneItems | MediaInputs | Vendors)
* @enumType EventSubscription
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
All = (General | Config | Scenes | Inputs | Transitions | Filters | Outputs | SceneItems | MediaInputs | Ui | Vendors),
/**
* Subscription value to receive the `InputVolumeMeters` high-volume event.
*
* @enumIdentifier InputVolumeMeters
* @enumValue (1 << 16)
* @enumType EventSubscription
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
InputVolumeMeters = (1 << 16),
/**
* Subscription value to receive the `InputActiveStateChanged` high-volume event.
*
* @enumIdentifier InputActiveStateChanged
* @enumValue (1 << 17)
* @enumType EventSubscription
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
InputActiveStateChanged = (1 << 17),
/**
* Subscription value to receive the `InputShowStateChanged` high-volume event.
*
* @enumIdentifier InputShowStateChanged
* @enumValue (1 << 18)
* @enumType EventSubscription
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
InputShowStateChanged = (1 << 18),
/**
* Subscription value to receive the `SceneItemTransformChanged` high-volume event.
*
* @enumIdentifier SceneItemTransformChanged
* @enumValue (1 << 19)
* @enumType EventSubscription
* @rpcVersion -1
* @initialVersion 5.0.0
* @api enums
*/
SceneItemTransformChanged = (1 << 19),
};
};
}

View File

@ -1,5 +1,25 @@
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#include <QClipboard>
#include <QPainter>
#include <QUrl>
#include <obs-module.h>
#include "ConnectInfo.h"
@ -7,7 +27,6 @@
#include "../obs-websocket.h"
#include "../Config.h"
#include "../utils/Platform.h"
#include "../plugin-macros.generated.h"
ConnectInfo::ConnectInfo(QWidget* parent) :
QDialog(parent, Qt::Dialog),
@ -28,7 +47,12 @@ ConnectInfo::~ConnectInfo()
delete ui;
}
void ConnectInfo::showEvent(QShowEvent *event)
void ConnectInfo::showEvent(QShowEvent *)
{
RefreshData();
}
void ConnectInfo::RefreshData()
{
auto conf = GetConfig();
if (!conf) {
@ -45,7 +69,7 @@ void ConnectInfo::showEvent(QShowEvent *event)
QString serverPassword;
if (conf->AuthRequired) {
ui->copyServerPasswordButton->setEnabled(true);
serverPassword = conf->ServerPassword;
serverPassword = QUrl::toPercentEncoding(conf->ServerPassword);
} else {
ui->copyServerPasswordButton->setEnabled(false);
serverPassword = obs_module_text("OBSWebSocket.ConnectInfo.ServerPasswordPlaceholderText");
@ -54,9 +78,9 @@ void ConnectInfo::showEvent(QShowEvent *event)
QString connectString;
if (conf->AuthRequired)
connectString = QString("obswebsocket|%1:%2|%3").arg(serverIp).arg(serverPort).arg(serverPassword);
connectString = QString("obsws://%1:%2/%3").arg(serverIp).arg(serverPort).arg(serverPassword);
else
connectString = QString("obswebsocket|%1:%2").arg(serverIp).arg(serverPort);
connectString = QString("obsws://%1:%2").arg(serverIp).arg(serverPort);
DrawQr(connectString);
}

View File

@ -1,7 +1,28 @@
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#pragma once
#include <QtWidgets/QDialog>
#include "../plugin-macros.generated.h"
#include "ui_ConnectInfo.h"
class ConnectInfo : public QDialog
@ -12,6 +33,7 @@ public:
explicit ConnectInfo(QWidget* parent = 0);
~ConnectInfo();
void showEvent(QShowEvent *event);
void RefreshData();
private Q_SLOTS:
void CopyServerIpButtonClicked();

View File

@ -1,3 +1,22 @@
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#include <QtWidgets/QMessageBox>
#include <QDateTime>
#include <QTime>
@ -7,9 +26,8 @@
#include "SettingsDialog.h"
#include "../obs-websocket.h"
#include "../Config.h"
#include "../WebSocketServer.h"
#include "../websocketserver/WebSocketServer.h"
#include "../utils/Crypto.h"
#include "../plugin-macros.generated.h"
QString GetToolTipIconHtml()
{
@ -57,7 +75,7 @@ SettingsDialog::~SettingsDialog()
delete sessionTableTimer;
}
void SettingsDialog::showEvent(QShowEvent *event)
void SettingsDialog::showEvent(QShowEvent *)
{
auto conf = GetConfig();
if (!conf) {
@ -91,7 +109,7 @@ void SettingsDialog::showEvent(QShowEvent *event)
sessionTableTimer->start(1000);
}
void SettingsDialog::closeEvent(QCloseEvent *event)
void SettingsDialog::hideEvent(QHideEvent *)
{
if (sessionTableTimer->isActive())
sessionTableTimer->stop();
@ -118,14 +136,21 @@ void SettingsDialog::DialogButtonClicked(QAbstractButton *button)
void SettingsDialog::SaveFormData()
{
connectInfo->hide();
auto conf = GetConfig();
if (!conf) {
blog(LOG_ERROR, "[SettingsDialog::SaveFormData] Unable to retreive config!");
return;
}
if (ui->serverPasswordLineEdit->text().length() < 6) {
QMessageBox msgBox;
msgBox.setWindowTitle(obs_module_text("OBSWebSocket.Settings.Save.PasswordInvalidErrorTitle"));
msgBox.setText(obs_module_text("OBSWebSocket.Settings.Save.PasswordInvalidErrorMessage"));
msgBox.setStandardButtons(QMessageBox::Ok);
msgBox.exec();
return;
}
// Show a confirmation box to the user if they attempt to provide their own password
if (passwordManuallyEdited && (conf->ServerPassword != ui->serverPasswordLineEdit->text())) {
QMessageBox msgBox;
@ -146,14 +171,9 @@ void SettingsDialog::SaveFormData()
}
}
bool needsRestart = false;
if ((conf->ServerEnabled != ui->enableWebSocketServerCheckBox->isChecked()) ||
(conf->DebugEnabled != ui->enableDebugLoggingCheckBox->isChecked()) ||
(conf->AuthRequired != ui->enableAuthenticationCheckBox->isChecked()) ||
(conf->ServerPassword != ui->serverPasswordLineEdit->text()) ||
(conf->ServerPort != ui->serverPortSpinBox->value())) {
needsRestart = true;
}
bool needsRestart = (conf->ServerEnabled != ui->enableWebSocketServerCheckBox->isChecked()) ||
(ui->enableAuthenticationCheckBox->isChecked() && conf->ServerPassword != ui->serverPasswordLineEdit->text()) ||
(conf->ServerPort != ui->serverPortSpinBox->value());
conf->ServerEnabled = ui->enableWebSocketServerCheckBox->isChecked();
conf->AlertsEnabled = ui->enableSystemTrayAlertsCheckBox->isChecked();
@ -164,6 +184,8 @@ void SettingsDialog::SaveFormData()
conf->Save();
connectInfo->RefreshData();
if (needsRestart) {
blog(LOG_INFO, "[SettingsDialog::SaveFormData] A setting was changed which requires a server restart.");
auto server = GetWebSocketServer();
@ -245,7 +267,7 @@ void SettingsDialog::EnableAuthenticationCheckBoxChanged()
void SettingsDialog::GeneratePasswordButtonClicked()
{
QString newPassword = Utils::Crypto::GeneratePassword();
QString newPassword = QString::fromStdString(Utils::Crypto::GeneratePassword());
ui->serverPasswordLineEdit->setText(newPassword);
ui->serverPasswordLineEdit->selectAll();
passwordManuallyEdited = false;

View File

@ -1,10 +1,31 @@
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#pragma once
#include <QDialog>
#include <QTimer>
#include "ui_SettingsDialog.h"
#include "ConnectInfo.h"
#include "../plugin-macros.generated.h"
#include "ui_SettingsDialog.h"
class SettingsDialog : public QDialog
{
@ -14,7 +35,7 @@ public:
explicit SettingsDialog(QWidget* parent = 0);
~SettingsDialog();
void showEvent(QShowEvent *event);
void closeEvent(QCloseEvent *event);
void hideEvent(QHideEvent *event);
void ToggleShowHide();
private Q_SLOTS:

View File

@ -151,7 +151,7 @@
<number>65534</number>
</property>
<property name="value">
<number>4444</number>
<number>4455</number>
</property>
</widget>
</item>

View File

@ -1,69 +1,84 @@
#include <QtCore/QTimer>
#include <QtWidgets/QAction>
#include <QtWidgets/QMainWindow>
#include <QTime>
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#include <QAction>
#include <QMainWindow>
#include <obs-module.h>
#include <obs-data.h>
#include <obs-frontend-api.h>
#include "plugin-macros.generated.h"
#include "obs-websocket.h"
#include "Config.h"
#include "WebSocketServer.h"
#include "WebSocketApi.h"
#include "websocketserver/WebSocketServer.h"
#include "eventhandler/EventHandler.h"
#include "forms/SettingsDialog.h"
OBS_DECLARE_MODULE()
OBS_MODULE_USE_DEFAULT_LOCALE("obs-websocket", "en-US")
OBS_MODULE_AUTHOR("OBSProject")
const char *obs_module_name(void) { return "obs-websocket"; }
const char *obs_module_description(void) { return obs_module_text("OBSWebSocket.Plugin.Description"); }
ConfigPtr _config;
WebSocketServerPtr _webSocketServer;
EventHandlerPtr _eventHandler;
SettingsDialog *_settingsDialog = nullptr;
os_cpu_usage_info_t* _cpuUsageInfo;
ConfigPtr _config;
EventHandlerPtr _eventHandler;
WebSocketApiPtr _webSocketApi;
WebSocketServerPtr _webSocketServer;
SettingsDialog *_settingsDialog = nullptr;
void ___source_dummy_addref(obs_source_t*) {}
void ___sceneitem_dummy_addref(obs_sceneitem_t*) {};
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); };
void WebSocketApiEventCallback(std::string vendorName, std::string eventType, obs_data_t *obsEventData);
bool obs_module_load(void)
{
blog(LOG_INFO, "[obs_module_load] you can haz websockets (Version: %s | RPC Version: %d)", OBS_WEBSOCKET_VERSION, OBS_WEBSOCKET_RPC_VERSION);
blog(LOG_INFO, "[obs_module_load] Qt version (compile-time): %s | Qt version (run-time): %s",
QT_VERSION_STR, qVersion());
blog(LOG_INFO, "[obs_module_load] Qt version (compile-time): %s | Qt version (run-time): %s", QT_VERSION_STR, qVersion());
blog(LOG_INFO, "[obs_module_load] Linked ASIO Version: %d", ASIO_VERSION);
// Randomize the random number generator
qsrand(QTime::currentTime().msec());
// Initialize the cpu stats
_cpuUsageInfo = os_cpu_usage_info_start();
// Create the config object then load the parameters from storage
// Create the config manager then load the parameters from storage
_config = ConfigPtr(new Config());
_config->Load();
// Initialize the event handler
_eventHandler = EventHandlerPtr(new EventHandler());
// Initialize the plugin/script API
_webSocketApi = WebSocketApiPtr(new WebSocketApi());
_webSocketApi->SetEventCallback(WebSocketApiEventCallback);
// Initialize the WebSocket server
_webSocketServer = WebSocketServerPtr(new WebSocketServer());
_eventHandler = EventHandlerPtr(new EventHandler(_webSocketServer));
// Initialize the settings dialog
obs_frontend_push_ui_translation(obs_module_get_string);
QMainWindow* mainWindow = reinterpret_cast<QMainWindow*>(obs_frontend_get_main_window());
QMainWindow* mainWindow = static_cast<QMainWindow*>(obs_frontend_get_main_window());
_settingsDialog = new SettingsDialog(mainWindow);
obs_frontend_pop_ui_translation();
// Add the settings dialog to the tools menu
const char* menuActionText = obs_module_text("OBSWebSocket.Settings.DialogTitle");
QAction* menuAction = (QAction*)obs_frontend_add_tools_menu_qaction(menuActionText);
QObject::connect(menuAction, &QAction::triggered, [] { _settingsDialog->ToggleShowHide(); });
_cpuUsageInfo = os_cpu_usage_info_start();
if (_config->ServerEnabled)
_webSocketServer->Start();
// Loading finished
blog(LOG_INFO, "[obs_module_load] Module loaded.");
return true;
}
@ -71,39 +86,122 @@ void obs_module_unload()
{
blog(LOG_INFO, "[obs_module_unload] Shutting down...");
// Shutdown the WebSocket server if it is running
if (_webSocketServer->IsListening()) {
blog(LOG_INFO, "[obs_module_unload] WebSocket server is running. Stopping...");
blog_debug("[obs_module_unload] WebSocket server is running. Stopping...");
_webSocketServer->Stop();
}
// Destroy the WebSocket server
_webSocketServer.reset();
// Destroy the plugin/script api
_webSocketApi.reset();
// Destroy the event handler
_eventHandler.reset();
_config->FirstLoad = false;
// Save and destroy the config manager
_config->Save();
_config.reset();
// Destroy the cpu stats
os_cpu_usage_info_destroy(_cpuUsageInfo);
blog(LOG_INFO, "[obs_module_unload] Finished shutting down.");
}
os_cpu_usage_info_t* GetCpuUsageInfo()
{
return _cpuUsageInfo;
}
ConfigPtr GetConfig()
{
return _config;
}
WebSocketServerPtr GetWebSocketServer()
{
return _webSocketServer;
}
EventHandlerPtr GetEventHandler()
{
return _eventHandler;
}
os_cpu_usage_info_t* GetCpuUsageInfo()
WebSocketApiPtr GetWebSocketApi()
{
return _cpuUsageInfo;
return _webSocketApi;
}
WebSocketServerPtr GetWebSocketServer()
{
return _webSocketServer;
}
bool IsDebugEnabled()
{
return !_config || _config->DebugEnabled;
}
/**
* An event has been emitted from a vendor.
*
* A vendor is a unique name registered by a third-party plugin or script, which allows for custom requests and events to be added to obs-websocket.
* If a plugin or script implements vendor requests or events, documentation is expected to be provided with them.
*
* @dataField vendorName | String | Name of the vendor emitting the event
* @dataField eventType | String | Vendor-provided event typedef
* @dataField eventData | Object | Vendor-provided event data. {} if event does not provide any data
*
* @eventSubscription Vendors
* @eventType VendorEvent
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api events
* @category general
*/
void WebSocketApiEventCallback(std::string vendorName, std::string eventType, obs_data_t *obsEventData)
{
json eventData = Utils::Json::ObsDataToJson(obsEventData);
json broadcastEventData;
broadcastEventData["vendorName"] = vendorName;
broadcastEventData["eventType"] = eventType;
broadcastEventData["eventData"] = eventData;
_webSocketServer->BroadcastEvent(EventSubscription::Vendors, "VendorEvent", broadcastEventData);
}
#ifdef PLUGIN_TESTS
static void test_vendor_request_cb(obs_data_t *requestData, obs_data_t *responseData, void *priv_data)
{
blog(LOG_INFO, "[test_vendor_request_cb] Request called!");
blog(LOG_INFO, "[test_vendor_request_cb] Request data: %s", obs_data_get_json(requestData));
// Set an item to the response data
obs_data_set_string(responseData, "test", "pp");
// Emit an event with the request data as the event data
obs_websocket_vendor_emit_event(priv_data, "TestEvent", requestData);
}
void obs_module_post_load()
{
blog(LOG_INFO, "[obs_module_post_load] Post load started.");
auto vendor = obs_websocket_register_vendor("obs-websocket-test");
if (!vendor) {
blog(LOG_WARNING, "[obs_module_post_load] Failed to create vendor!");
return;
}
if (!obs_websocket_vendor_register_request(vendor, "TestRequest", test_vendor_request_cb, vendor)) {
blog(LOG_WARNING, "[obs_module_post_load] Failed to register vendor request!");
return;
}
blog(LOG_INFO, "[obs_module_post_load] Post load completed.");
}
#endif

View File

@ -1,50 +1,51 @@
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#pragma once
#include <memory>
#include <obs.hpp>
#ifdef _MSC_VER
#pragma push_macro("strtoll")
#endif
#include <util/platform.h>
#ifdef _MSC_VER
#pragma pop_macro("strtoll")
#endif
// Autorelease object definitions
void ___source_dummy_addref(obs_source_t*);
void ___sceneitem_dummy_addref(obs_sceneitem_t*);
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*);
using OBSSourceAutoRelease =
OBSRef<obs_source_t*, ___source_dummy_addref, obs_source_release>;
using OBSSceneItemAutoRelease =
OBSRef<obs_sceneitem_t*, ___sceneitem_dummy_addref, obs_sceneitem_release>;
using OBSDataAutoRelease =
OBSRef<obs_data_t*, ___data_dummy_addref, obs_data_release>;
using OBSDataArrayAutoRelease =
OBSRef<obs_data_array_t*, ___data_array_dummy_addref, obs_data_array_release>;
using OBSOutputAutoRelease =
OBSRef<obs_output_t*, ___output_dummy_addref, obs_output_release>;
using OBSDataItemAutoRelease =
OBSRef<obs_data_item_t*, ___data_item_dummy_addref, ___data_item_release>;
#include "utils/Obs.h"
#include "plugin-macros.generated.h"
class Config;
typedef std::shared_ptr<Config> ConfigPtr;
class WebSocketServer;
typedef std::shared_ptr<WebSocketServer> WebSocketServerPtr;
class EventHandler;
typedef std::shared_ptr<EventHandler> EventHandlerPtr;
ConfigPtr GetConfig();
class WebSocketApi;
typedef std::shared_ptr<WebSocketApi> WebSocketApiPtr;
WebSocketServerPtr GetWebSocketServer();
class WebSocketServer;
typedef std::shared_ptr<WebSocketServer> WebSocketServerPtr;
os_cpu_usage_info_t* GetCpuUsageInfo();
ConfigPtr GetConfig();
EventHandlerPtr GetEventHandler();
os_cpu_usage_info_t* GetCpuUsageInfo();
WebSocketApiPtr GetWebSocketApi();
WebSocketServerPtr GetWebSocketServer();
bool IsDebugEnabled();

View File

@ -1,6 +1,7 @@
/*
obs-websocket
Copyright (C) 2021 Kyle Manning <tt2468@irltoolkit.com>
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -21,7 +22,9 @@ with this program. If not, see <https://www.gnu.org/licenses/>
#include <util/base.h>
#define blog(level, msg, ...) blog(level, "[obs-websocket] " msg, ##__VA_ARGS__)
#define OBS_WEBSOCKET_VERSION "@CMAKE_PROJECT_VERSION@"
#define blog_debug(msg, ...) if (IsDebugEnabled()) blog(LOG_INFO, "[debug] " msg, ##__VA_ARGS__)
#define OBS_WEBSOCKET_VERSION "@OBS_WEBSOCKET_VERSION@"
#define OBS_WEBSOCKET_RPC_VERSION @OBS_WEBSOCKET_RPC_VERSION@

View File

@ -0,0 +1,225 @@
/*
obs-websocket
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#include <queue>
#include <condition_variable>
#include <util/profiler.hpp>
#include "RequestBatchHandler.h"
#include "../utils/Compat.h"
#include "../obs-websocket.h"
struct SerialFrameBatch
{
RequestHandler &requestHandler;
std::queue<RequestBatchRequest> requests;
std::vector<RequestResult> results;
json &variables;
bool haltOnFailure;
size_t frameCount;
size_t sleepUntilFrame;
std::mutex conditionMutex;
std::condition_variable condition;
SerialFrameBatch(RequestHandler &requestHandler, json &variables, bool haltOnFailure) :
requestHandler(requestHandler),
variables(variables),
haltOnFailure(haltOnFailure),
frameCount(0),
sleepUntilFrame(0)
{}
};
struct ParallelBatchResults
{
RequestHandler &requestHandler;
std::vector<RequestResult> results;
std::mutex conditionMutex;
std::condition_variable condition;
ParallelBatchResults(RequestHandler &requestHandler) :
requestHandler(requestHandler)
{}
};
// `{"inputName": "inputNameVariable"}` is essentially `inputName = inputNameVariable`
static void PreProcessVariables(const json &variables, RequestBatchRequest &request)
{
if (variables.empty() || !request.InputVariables.is_object() || request.InputVariables.empty() || !request.RequestData.is_object())
return;
for (auto& [key, value] : request.InputVariables.items()) {
if (!value.is_string()) {
blog_debug("[WebSocketServer::ProcessRequestBatch] Value of field `%s` in `inputVariables `is not a string. Skipping!", key.c_str());
continue;
}
std::string valueString = value;
if (!variables.contains(valueString)) {
blog_debug("[WebSocketServer::ProcessRequestBatch] `inputVariables` requested variable `%s`, but it does not exist. Skipping!", valueString.c_str());
continue;
}
request.RequestData[key] = variables[valueString];
}
request.HasRequestData = !request.RequestData.empty();
}
// `{"sceneItemIdVariable": "sceneItemId"}` is essentially `sceneItemIdVariable = sceneItemId`
static void PostProcessVariables(json &variables, const RequestBatchRequest &request, const RequestResult &requestResult)
{
if (!request.OutputVariables.is_object() || request.OutputVariables.empty() || requestResult.ResponseData.empty())
return;
for (auto& [key, value] : request.OutputVariables.items()) {
if (!value.is_string()) {
blog_debug("[WebSocketServer::ProcessRequestBatch] Value of field `%s` in `outputVariables` is not a string. Skipping!", key.c_str());
continue;
}
std::string valueString = value;
if (!requestResult.ResponseData.contains(valueString)) {
blog_debug("[WebSocketServer::ProcessRequestBatch] `outputVariables` requested responseData field `%s`, but it does not exist. Skipping!", valueString.c_str());
continue;
}
variables[key] = requestResult.ResponseData[valueString];
}
}
static void ObsTickCallback(void *param, float)
{
ScopeProfiler prof{"obs_websocket_request_batch_frame_tick"};
auto serialFrameBatch = static_cast<SerialFrameBatch*>(param);
// Increment frame count
serialFrameBatch->frameCount++;
if (serialFrameBatch->sleepUntilFrame) {
if (serialFrameBatch->frameCount < serialFrameBatch->sleepUntilFrame)
// Do not process any requests if in "sleep mode"
return;
else
// Reset frame sleep until counter if not being used
serialFrameBatch->sleepUntilFrame = 0;
}
// Begin recursing any unprocessed requests
while (!serialFrameBatch->requests.empty()) {
// Fetch first in queue
RequestBatchRequest request = serialFrameBatch->requests.front();
// Pre-process batch variables
PreProcessVariables(serialFrameBatch->variables, request);
// Process request and get result
RequestResult requestResult = serialFrameBatch->requestHandler.ProcessRequest(request);
// Post-process batch variables
PostProcessVariables(serialFrameBatch->variables, request, requestResult);
// Add to results vector
serialFrameBatch->results.push_back(requestResult);
// Remove from front of queue
serialFrameBatch->requests.pop();
// If haltOnFailure and the request failed, clear the queue to make the batch return early.
if (serialFrameBatch->haltOnFailure && requestResult.StatusCode != RequestStatus::Success) {
serialFrameBatch->requests = std::queue<RequestBatchRequest>();
break;
}
// If the processed request tells us to sleep, do so accordingly
if (requestResult.SleepFrames) {
serialFrameBatch->sleepUntilFrame = serialFrameBatch->frameCount + requestResult.SleepFrames;
break;
}
}
// If request queue is empty, we can notify the paused worker thread
if (serialFrameBatch->requests.empty())
serialFrameBatch->condition.notify_one();
}
std::vector<RequestResult> RequestBatchHandler::ProcessRequestBatch(QThreadPool &threadPool, SessionPtr session, RequestBatchExecutionType::RequestBatchExecutionType executionType, std::vector<RequestBatchRequest> &requests, json &variables, bool haltOnFailure)
{
RequestHandler requestHandler(session);
if (executionType == RequestBatchExecutionType::SerialRealtime) {
std::vector<RequestResult> ret;
// Recurse all requests in batch serially, processing the request then moving to the next one
for (auto &request : requests) {
PreProcessVariables(variables, request);
RequestResult requestResult = requestHandler.ProcessRequest(request);
PostProcessVariables(variables, request, requestResult);
ret.push_back(requestResult);
if (haltOnFailure && requestResult.StatusCode != RequestStatus::Success)
break;
}
return ret;
} else if (executionType == RequestBatchExecutionType::SerialFrame) {
SerialFrameBatch serialFrameBatch(requestHandler, variables, haltOnFailure);
// Create Request objects in the worker thread (avoid unnecessary processing in graphics thread)
for (auto &request : requests)
serialFrameBatch.requests.push(request);
// Create a callback entry for the graphics thread to execute on each video frame
obs_add_tick_callback(ObsTickCallback, &serialFrameBatch);
// Wait until the graphics thread processes the last request in the queue
std::unique_lock<std::mutex> lock(serialFrameBatch.conditionMutex);
serialFrameBatch.condition.wait(lock, [&serialFrameBatch]{return serialFrameBatch.requests.empty();});
// Remove the created callback entry since we don't need it anymore
obs_remove_tick_callback(ObsTickCallback, &serialFrameBatch);
return serialFrameBatch.results;
} else if (executionType == RequestBatchExecutionType::Parallel) {
ParallelBatchResults parallelResults(requestHandler);
// Acquire the lock early to prevent the batch from finishing before we're ready
std::unique_lock<std::mutex> lock(parallelResults.conditionMutex);
// Submit each request as a task to the thread pool to be processed ASAP
for (auto &request : requests) {
threadPool.start(Utils::Compat::CreateFunctionRunnable([&parallelResults, &request]() {
RequestResult requestResult = parallelResults.requestHandler.ProcessRequest(request);
std::unique_lock<std::mutex> lock(parallelResults.conditionMutex);
parallelResults.results.push_back(requestResult);
lock.unlock();
parallelResults.condition.notify_one();
}));
}
// Wait for the last request to finish processing
size_t requestCount = requests.size();
parallelResults.condition.wait(lock, [&parallelResults, requestCount]{return parallelResults.results.size() == requestCount;});
return parallelResults.results;
}
// Return empty vector if not a batch somehow
return std::vector<RequestResult>();
}

View File

@ -0,0 +1,28 @@
/*
obs-websocket
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#pragma once
#include <QThreadPool>
#include "RequestHandler.h"
#include "rpc/RequestBatchRequest.h"
namespace RequestBatchHandler {
std::vector<RequestResult> ProcessRequestBatch(QThreadPool &threadPool, SessionPtr session, RequestBatchExecutionType::RequestBatchExecutionType executionType, std::vector<RequestBatchRequest> &requests, json &variables, bool haltOnFailure);
}

View File

@ -1,17 +1,38 @@
#include "RequestHandler.h"
#include "../plugin-macros.generated.h"
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
const std::map<std::string, RequestMethodHandler> RequestHandler::_handlerMap
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#ifdef PLUGIN_TESTS
#include <util/profiler.hpp>
#endif
#include "RequestHandler.h"
const std::unordered_map<std::string, RequestMethodHandler> RequestHandler::_handlerMap
{
// General
{"GetVersion", &RequestHandler::GetVersion},
{"BroadcastCustomEvent", &RequestHandler::BroadcastCustomEvent},
{"GetStats", &RequestHandler::GetStats},
{"BroadcastCustomEvent", &RequestHandler::BroadcastCustomEvent},
{"CallVendorRequest", &RequestHandler::CallVendorRequest},
{"GetHotkeyList", &RequestHandler::GetHotkeyList},
{"TriggerHotkeyByName", &RequestHandler::TriggerHotkeyByName},
{"TriggerHotkeyByKeySequence", &RequestHandler::TriggerHotkeyByKeySequence},
{"GetStudioModeEnabled", &RequestHandler::GetStudioModeEnabled},
{"SetStudioModeEnabled", &RequestHandler::SetStudioModeEnabled},
{"Sleep", &RequestHandler::Sleep},
// Config
@ -30,14 +51,18 @@ const std::map<std::string, RequestMethodHandler> RequestHandler::_handlerMap
{"SetVideoSettings", &RequestHandler::SetVideoSettings},
{"GetStreamServiceSettings", &RequestHandler::GetStreamServiceSettings},
{"SetStreamServiceSettings", &RequestHandler::SetStreamServiceSettings},
{"GetRecordDirectory", &RequestHandler::GetRecordDirectory},
// Sources
{"GetSourceActive", &RequestHandler::GetSourceActive},
{"GetSourceScreenshot", &RequestHandler::GetSourceScreenshot},
{"SaveSourceScreenshot", &RequestHandler::SaveSourceScreenshot},
{"GetSourcePrivateSettings", &RequestHandler::GetSourcePrivateSettings},
{"SetSourcePrivateSettings", &RequestHandler::SetSourcePrivateSettings},
// Scenes
{"GetSceneList", &RequestHandler::GetSceneList},
{"GetGroupList", &RequestHandler::GetGroupList},
{"GetCurrentProgramScene", &RequestHandler::GetCurrentProgramScene},
{"SetCurrentProgramScene", &RequestHandler::SetCurrentProgramScene},
{"GetCurrentPreviewScene", &RequestHandler::GetCurrentPreviewScene},
@ -45,11 +70,15 @@ const std::map<std::string, RequestMethodHandler> RequestHandler::_handlerMap
{"CreateScene", &RequestHandler::CreateScene},
{"RemoveScene", &RequestHandler::RemoveScene},
{"SetSceneName", &RequestHandler::SetSceneName},
{"GetSceneSceneTransitionOverride", &RequestHandler::GetSceneSceneTransitionOverride},
{"SetSceneSceneTransitionOverride", &RequestHandler::SetSceneSceneTransitionOverride},
// Inputs
{"GetInputList", &RequestHandler::GetInputList},
{"GetInputKindList", &RequestHandler::GetInputKindList},
{"GetSpecialInputs", &RequestHandler::GetSpecialInputs},
{"CreateInput", &RequestHandler::CreateInput},
{"RemoveInput", &RequestHandler::RemoveInput},
{"SetInputName", &RequestHandler::SetInputName},
{"GetInputDefaultSettings", &RequestHandler::GetInputDefaultSettings},
{"GetInputSettings", &RequestHandler::GetInputSettings},
@ -59,17 +88,112 @@ const std::map<std::string, RequestMethodHandler> RequestHandler::_handlerMap
{"ToggleInputMute", &RequestHandler::ToggleInputMute},
{"GetInputVolume", &RequestHandler::GetInputVolume},
{"SetInputVolume", &RequestHandler::SetInputVolume},
{"GetInputAudioBalance", &RequestHandler::GetInputAudioBalance},
{"SetInputAudioBalance", &RequestHandler::SetInputAudioBalance},
{"GetInputAudioSyncOffset", &RequestHandler::GetInputAudioSyncOffset},
{"SetInputAudioSyncOffset", &RequestHandler::SetInputAudioSyncOffset},
{"GetInputAudioMonitorType", &RequestHandler::GetInputAudioMonitorType},
{"SetInputAudioMonitorType", &RequestHandler::SetInputAudioMonitorType},
{"GetInputAudioTracks", &RequestHandler::GetInputAudioTracks},
{"SetInputAudioTracks", &RequestHandler::SetInputAudioTracks},
{"GetInputPropertiesListPropertyItems", &RequestHandler::GetInputPropertiesListPropertyItems},
{"PressInputPropertiesButton", &RequestHandler::PressInputPropertiesButton},
// Transitions
{"GetTransitionKindList", &RequestHandler::GetTransitionKindList},
{"GetSceneTransitionList", &RequestHandler::GetSceneTransitionList},
{"GetCurrentSceneTransition", &RequestHandler::GetCurrentSceneTransition},
{"SetCurrentSceneTransition", &RequestHandler::SetCurrentSceneTransition},
{"SetCurrentSceneTransitionDuration", &RequestHandler::SetCurrentSceneTransitionDuration},
{"SetCurrentSceneTransitionSettings", &RequestHandler::SetCurrentSceneTransitionSettings},
{"GetCurrentSceneTransitionCursor", &RequestHandler::GetCurrentSceneTransitionCursor},
{"TriggerStudioModeTransition", &RequestHandler::TriggerStudioModeTransition},
{"SetTBarPosition", &RequestHandler::SetTBarPosition},
// Filters
{"GetSourceFilterList", &RequestHandler::GetSourceFilterList},
{"GetSourceFilterDefaultSettings", &RequestHandler::GetSourceFilterDefaultSettings},
{"CreateSourceFilter", &RequestHandler::CreateSourceFilter},
{"RemoveSourceFilter", &RequestHandler::RemoveSourceFilter},
{"SetSourceFilterName", &RequestHandler::SetSourceFilterName},
{"GetSourceFilter", &RequestHandler::GetSourceFilter},
{"SetSourceFilterIndex", &RequestHandler::SetSourceFilterIndex},
{"SetSourceFilterSettings", &RequestHandler::SetSourceFilterSettings},
{"SetSourceFilterEnabled", &RequestHandler::SetSourceFilterEnabled},
// Scene Items
{"GetSceneItemList", &RequestHandler::GetSceneItemList},
{"GetGroupSceneItemList", &RequestHandler::GetGroupSceneItemList},
{"GetSceneItemId", &RequestHandler::GetSceneItemId},
{"CreateSceneItem", &RequestHandler::CreateSceneItem},
{"RemoveSceneItem", &RequestHandler::RemoveSceneItem},
{"DuplicateSceneItem", &RequestHandler::DuplicateSceneItem},
{"GetSceneItemTransform", &RequestHandler::GetSceneItemTransform},
{"SetSceneItemTransform", &RequestHandler::SetSceneItemTransform},
{"GetSceneItemEnabled", &RequestHandler::GetSceneItemEnabled},
{"SetSceneItemEnabled", &RequestHandler::SetSceneItemEnabled},
{"GetSceneItemLocked", &RequestHandler::GetSceneItemLocked},
{"SetSceneItemLocked", &RequestHandler::SetSceneItemLocked},
{"GetSceneItemIndex", &RequestHandler::GetSceneItemIndex},
{"SetSceneItemIndex", &RequestHandler::SetSceneItemIndex},
{"GetSceneItemBlendMode", &RequestHandler::GetSceneItemBlendMode},
{"SetSceneItemBlendMode", &RequestHandler::SetSceneItemBlendMode},
// Outputs
{"GetVirtualCamStatus", &RequestHandler::GetVirtualCamStatus},
{"ToggleVirtualCam", &RequestHandler::ToggleVirtualCam},
{"StartVirtualCam", &RequestHandler::StartVirtualCam},
{"StopVirtualCam", &RequestHandler::StopVirtualCam},
{"GetReplayBufferStatus", &RequestHandler::GetReplayBufferStatus},
{"ToggleReplayBuffer", &RequestHandler::ToggleReplayBuffer},
{"StartReplayBuffer", &RequestHandler::StartReplayBuffer},
{"StopReplayBuffer", &RequestHandler::StopReplayBuffer},
{"SaveReplayBuffer", &RequestHandler::SaveReplayBuffer},
{"GetLastReplayBufferReplay", &RequestHandler::GetLastReplayBufferReplay},
// Stream
{"GetStreamStatus", &RequestHandler::GetStreamStatus},
{"ToggleStream", &RequestHandler::ToggleStream},
{"StartStream", &RequestHandler::StartStream},
{"StopStream", &RequestHandler::StopStream},
{"SendStreamCaption", &RequestHandler::SendStreamCaption},
// Record
{"GetRecordStatus", &RequestHandler::GetRecordStatus},
{"ToggleRecord", &RequestHandler::ToggleRecord},
{"StartRecord", &RequestHandler::StartRecord},
{"StopRecord", &RequestHandler::StopRecord},
{"ToggleRecordPause", &RequestHandler::ToggleRecordPause},
{"PauseRecord", &RequestHandler::PauseRecord},
{"ResumeRecord", &RequestHandler::ResumeRecord},
// Media Inputs
{"GetMediaInputStatus", &RequestHandler::GetMediaInputStatus},
{"SetMediaInputCursor", &RequestHandler::SetMediaInputCursor},
{"OffsetMediaInputCursor", &RequestHandler::OffsetMediaInputCursor},
{"TriggerMediaInputAction", &RequestHandler::TriggerMediaInputAction},
// Ui
{"GetStudioModeEnabled", &RequestHandler::GetStudioModeEnabled},
{"SetStudioModeEnabled", &RequestHandler::SetStudioModeEnabled},
{"OpenInputPropertiesDialog", &RequestHandler::OpenInputPropertiesDialog},
{"OpenInputFiltersDialog", &RequestHandler::OpenInputFiltersDialog},
{"OpenInputInteractDialog", &RequestHandler::OpenInputInteractDialog},
};
RequestHandler::RequestHandler(SessionPtr session) :
_session(session)
{
}
RequestResult RequestHandler::ProcessRequest(const Request& request)
{
if (!request.RequestData.is_null() && !request.RequestData.is_object())
return RequestResult::Error(RequestStatus::InvalidRequestParameterDataType, "Your request data is not an object.");
#ifdef PLUGIN_TESTS
ScopeProfiler prof{"obs_websocket_request_processing"};
#endif
if (!request.RequestData.is_object() && !request.RequestData.is_null())
return RequestResult::Error(RequestStatus::InvalidRequestFieldType, "Your request data is not an object.");
if (request.RequestType.empty())
return RequestResult::Error(RequestStatus::MissingRequestType, "Your request is missing a `requestType`");

View File

@ -1,32 +1,56 @@
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#pragma once
#include <map>
#include <unordered_map>
#include <obs.hpp>
#include <obs-frontend-api.h>
#include "rpc/Request.h"
#include "rpc/RequestResult.h"
#include "types/RequestStatus.h"
#include "types/RequestBatchExecutionType.h"
#include "../websocketserver/rpc/WebSocketSession.h"
#include "../obs-websocket.h"
#include "../utils/Obs.h"
#include "../plugin-macros.generated.h"
class RequestHandler;
typedef RequestResult(RequestHandler::*RequestMethodHandler)(const Request&);
class RequestHandler {
public:
RequestHandler(SessionPtr session);
RequestResult ProcessRequest(const Request& request);
std::vector<std::string> GetRequestList();
private:
// General
RequestResult GetVersion(const Request&);
RequestResult BroadcastCustomEvent(const Request&);
RequestResult GetStats(const Request&);
RequestResult BroadcastCustomEvent(const Request&);
RequestResult CallVendorRequest(const Request&);
RequestResult GetHotkeyList(const Request&);
RequestResult TriggerHotkeyByName(const Request&);
RequestResult TriggerHotkeyByKeySequence(const Request&);
RequestResult GetStudioModeEnabled(const Request&);
RequestResult SetStudioModeEnabled(const Request&);
RequestResult Sleep(const Request&);
// Config
@ -45,14 +69,18 @@ class RequestHandler {
RequestResult SetVideoSettings(const Request&);
RequestResult GetStreamServiceSettings(const Request&);
RequestResult SetStreamServiceSettings(const Request&);
RequestResult GetRecordDirectory(const Request&);
// Sources
RequestResult GetSourceActive(const Request&);
RequestResult GetSourceScreenshot(const Request&);
RequestResult SaveSourceScreenshot(const Request&);
RequestResult GetSourcePrivateSettings(const Request&);
RequestResult SetSourcePrivateSettings(const Request&);
// Scenes
RequestResult GetSceneList(const Request&);
RequestResult GetGroupList(const Request&);
RequestResult GetCurrentProgramScene(const Request&);
RequestResult SetCurrentProgramScene(const Request&);
RequestResult GetCurrentPreviewScene(const Request&);
@ -60,11 +88,15 @@ class RequestHandler {
RequestResult CreateScene(const Request&);
RequestResult RemoveScene(const Request&);
RequestResult SetSceneName(const Request&);
RequestResult GetSceneSceneTransitionOverride(const Request&);
RequestResult SetSceneSceneTransitionOverride(const Request&);
// Inputs
RequestResult GetInputList(const Request&);
RequestResult GetInputKindList(const Request&);
RequestResult GetSpecialInputs(const Request&);
RequestResult CreateInput(const Request&);
RequestResult RemoveInput(const Request&);
RequestResult SetInputName(const Request&);
RequestResult GetInputDefaultSettings(const Request&);
RequestResult GetInputSettings(const Request&);
@ -74,11 +106,98 @@ class RequestHandler {
RequestResult ToggleInputMute(const Request&);
RequestResult GetInputVolume(const Request&);
RequestResult SetInputVolume(const Request&);
RequestResult GetInputAudioBalance(const Request&);
RequestResult SetInputAudioBalance(const Request&);
RequestResult GetInputAudioSyncOffset(const Request&);
RequestResult SetInputAudioSyncOffset(const Request&);
RequestResult GetInputAudioMonitorType(const Request&);
RequestResult SetInputAudioMonitorType(const Request&);
RequestResult GetInputAudioTracks(const Request&);
RequestResult SetInputAudioTracks(const Request&);
RequestResult GetInputPropertiesListPropertyItems(const Request&);
RequestResult PressInputPropertiesButton(const Request&);
// Transitions
RequestResult GetTransitionKindList(const Request&);
RequestResult GetSceneTransitionList(const Request&);
RequestResult GetCurrentSceneTransition(const Request&);
RequestResult SetCurrentSceneTransition(const Request&);
RequestResult SetCurrentSceneTransitionDuration(const Request&);
RequestResult SetCurrentSceneTransitionSettings(const Request&);
RequestResult GetCurrentSceneTransitionCursor(const Request&);
RequestResult TriggerStudioModeTransition(const Request&);
RequestResult SetTBarPosition(const Request&);
// Filters
RequestResult GetSourceFilterList(const Request&);
RequestResult GetSourceFilterDefaultSettings(const Request&);
RequestResult CreateSourceFilter(const Request&);
RequestResult RemoveSourceFilter(const Request&);
RequestResult SetSourceFilterName(const Request&);
RequestResult GetSourceFilter(const Request&);
RequestResult SetSourceFilterIndex(const Request&);
RequestResult SetSourceFilterSettings(const Request&);
RequestResult SetSourceFilterEnabled(const Request&);
// Scene Items
RequestResult GetSceneItemList(const Request&);
RequestResult GetGroupSceneItemList(const Request&);
RequestResult GetSceneItemId(const Request&);
RequestResult CreateSceneItem(const Request&);
RequestResult RemoveSceneItem(const Request&);
RequestResult DuplicateSceneItem(const Request&);
RequestResult GetSceneItemTransform(const Request&);
RequestResult SetSceneItemTransform(const Request&);
RequestResult GetSceneItemEnabled(const Request&);
RequestResult SetSceneItemEnabled(const Request&);
RequestResult GetSceneItemLocked(const Request&);
RequestResult SetSceneItemLocked(const Request&);
RequestResult GetSceneItemIndex(const Request&);
RequestResult SetSceneItemIndex(const Request&);
RequestResult GetSceneItemBlendMode(const Request&);
RequestResult SetSceneItemBlendMode(const Request&);
// Outputs
RequestResult GetVirtualCamStatus(const Request&);
RequestResult ToggleVirtualCam(const Request&);
RequestResult StartVirtualCam(const Request&);
RequestResult StopVirtualCam(const Request&);
RequestResult GetReplayBufferStatus(const Request&);
RequestResult ToggleReplayBuffer(const Request&);
RequestResult StartReplayBuffer(const Request&);
RequestResult StopReplayBuffer(const Request&);
RequestResult SaveReplayBuffer(const Request&);
RequestResult GetLastReplayBufferReplay(const Request&);
// Stream
RequestResult GetStreamStatus(const Request&);
RequestResult ToggleStream(const Request&);
RequestResult StartStream(const Request&);
RequestResult StopStream(const Request&);
RequestResult SendStreamCaption(const Request&);
static const std::map<std::string, RequestMethodHandler> _handlerMap;
// Record
RequestResult GetRecordStatus(const Request&);
RequestResult ToggleRecord(const Request&);
RequestResult StartRecord(const Request&);
RequestResult StopRecord(const Request&);
RequestResult ToggleRecordPause(const Request&);
RequestResult PauseRecord(const Request&);
RequestResult ResumeRecord(const Request&);
// Media Inputs
RequestResult GetMediaInputStatus(const Request&);
RequestResult SetMediaInputCursor(const Request&);
RequestResult OffsetMediaInputCursor(const Request&);
RequestResult TriggerMediaInputAction(const Request&);
// Ui
RequestResult GetStudioModeEnabled(const Request&);
RequestResult SetStudioModeEnabled(const Request&);
RequestResult OpenInputPropertiesDialog(const Request&);
RequestResult OpenInputFiltersDialog(const Request&);
RequestResult OpenInputInteractDialog(const Request&);
SessionPtr _session;
static const std::unordered_map<std::string, RequestMethodHandler> _handlerMap;
};

View File

@ -1,9 +1,42 @@
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#include <QMainWindow>
#include <util/config-file.h>
#include "RequestHandler.h"
#include "../plugin-macros.generated.h"
/**
* Gets the value of a "slot" from the selected persistent data realm.
*
* @requestField realm | String | The data realm to select. `OBS_WEBSOCKET_DATA_REALM_GLOBAL` or `OBS_WEBSOCKET_DATA_REALM_PROFILE`
* @requestField slotName | String | The name of the slot to retrieve data from
*
* @responseField slotValue | Any | Value associated with the slot. `null` if not set
*
* @requestType GetPersistentData
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api requests
*/
RequestResult RequestHandler::GetPersistentData(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -16,27 +49,41 @@ RequestResult RequestHandler::GetPersistentData(const Request& request)
std::string persistentDataPath = Utils::Obs::StringHelper::GetCurrentProfilePath();
if (realm == "OBS_WEBSOCKET_DATA_REALM_GLOBAL")
persistentDataPath += "../../../obsWebSocketPersistentData.json";
persistentDataPath += "/../../../obsWebSocketPersistentData.json";
else if (realm == "OBS_WEBSOCKET_DATA_REALM_PROFILE")
persistentDataPath += "/obsWebSocketPersistentData.json";
else
return RequestResult::Error(RequestStatus::DataRealmNotFound, "You have specified an invalid persistent data realm.");
return RequestResult::Error(RequestStatus::ResourceNotFound, "You have specified an invalid persistent data realm.");
json responseData;
json persistentData;
if (!(Utils::Json::GetJsonFileContent(persistentDataPath, persistentData) && persistentData.contains(slotName)))
responseData["slotValue"] = nullptr;
else
if (Utils::Json::GetJsonFileContent(persistentDataPath, persistentData) && persistentData.contains(slotName))
responseData["slotValue"] = persistentData[slotName];
else
responseData["slotValue"] = nullptr;
return RequestResult::Success(responseData);
}
/**
* Sets the value of a "slot" from the selected persistent data realm.
*
* @requestField realm | String | The data realm to select. `OBS_WEBSOCKET_DATA_REALM_GLOBAL` or `OBS_WEBSOCKET_DATA_REALM_PROFILE`
* @requestField slotName | String | The name of the slot to retrieve data from
* @requestField slotValue | Any | The value to apply to the slot
*
* @requestType SetPersistentData
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api requests
*/
RequestResult RequestHandler::SetPersistentData(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
if (!(request.ValidateString("realm", statusCode, comment) && request.ValidateString("slotName", statusCode, comment) && request.ValidateBasic("slotName", statusCode, comment)))
if (!(request.ValidateString("realm", statusCode, comment) && request.ValidateString("slotName", statusCode, comment) && request.ValidateBasic("slotValue", statusCode, comment)))
return RequestResult::Error(statusCode, comment);
std::string realm = request.RequestData["realm"];
@ -45,11 +92,11 @@ RequestResult RequestHandler::SetPersistentData(const Request& request)
std::string persistentDataPath = Utils::Obs::StringHelper::GetCurrentProfilePath();
if (realm == "OBS_WEBSOCKET_DATA_REALM_GLOBAL")
persistentDataPath += "../../../obsWebSocketPersistentData.json";
persistentDataPath += "/../../../obsWebSocketPersistentData.json";
else if (realm == "OBS_WEBSOCKET_DATA_REALM_PROFILE")
persistentDataPath += "/obsWebSocketPersistentData.json";
else
return RequestResult::Error(RequestStatus::DataRealmNotFound, "You have specified an invalid persistent data realm.");
return RequestResult::Error(RequestStatus::ResourceNotFound, "You have specified an invalid persistent data realm.");
json persistentData = json::object();
Utils::Json::GetJsonFileContent(persistentDataPath, persistentData);
@ -60,14 +107,41 @@ RequestResult RequestHandler::SetPersistentData(const Request& request)
return RequestResult::Success();
}
RequestResult RequestHandler::GetSceneCollectionList(const Request& request)
/**
* Gets an array of all scene collections
*
* @responseField currentSceneCollectionName | String | The name of the current scene collection
* @responseField sceneCollections | Array<String> | Array of all available scene collections
*
* @requestType GetSceneCollectionList
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api requests
*/
RequestResult RequestHandler::GetSceneCollectionList(const Request&)
{
json responseData;
responseData["currentSceneCollectionName"] = Utils::Obs::StringHelper::GetCurrentSceneCollection();
responseData["sceneCollections"] = Utils::Obs::ListHelper::GetSceneCollectionList();
responseData["sceneCollections"] = Utils::Obs::ArrayHelper::GetSceneCollectionList();
return RequestResult::Success(responseData);
}
/**
* Switches to a scene collection.
*
* Note: This will block until the collection has finished changing.
*
* @requestField sceneCollectionName | String | Name of the scene collection to switch to
*
* @requestType SetCurrentSceneCollection
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api requests
*/
RequestResult RequestHandler::SetCurrentSceneCollection(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -77,21 +151,35 @@ RequestResult RequestHandler::SetCurrentSceneCollection(const Request& request)
std::string sceneCollectionName = request.RequestData["sceneCollectionName"];
auto sceneCollections = Utils::Obs::ListHelper::GetSceneCollectionList();
auto sceneCollections = Utils::Obs::ArrayHelper::GetSceneCollectionList();
if (std::find(sceneCollections.begin(), sceneCollections.end(), sceneCollectionName) == sceneCollections.end())
return RequestResult::Error(RequestStatus::SceneCollectionNotFound);
return RequestResult::Error(RequestStatus::ResourceNotFound);
std::string currentSceneCollectionName = Utils::Obs::StringHelper::GetCurrentSceneCollection();
// Avoid queueing tasks if nothing will change
if (currentSceneCollectionName != sceneCollectionName) {
obs_queue_task(OBS_TASK_UI, [](void* param) {
obs_frontend_set_current_scene_collection(reinterpret_cast<const char*>(param));
obs_frontend_set_current_scene_collection(static_cast<const char*>(param));
}, (void*)sceneCollectionName.c_str(), true);
}
return RequestResult::Success();
}
/**
* Creates a new scene collection, switching to it in the process.
*
* Note: This will block until the collection has finished changing.
*
* @requestField sceneCollectionName | String | Name for the new scene collection
*
* @requestType CreateSceneCollection
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api requests
*/
RequestResult RequestHandler::CreateSceneCollection(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -101,27 +189,52 @@ RequestResult RequestHandler::CreateSceneCollection(const Request& request)
std::string sceneCollectionName = request.RequestData["sceneCollectionName"];
auto sceneCollections = Utils::Obs::ListHelper::GetSceneCollectionList();
auto sceneCollections = Utils::Obs::ArrayHelper::GetSceneCollectionList();
if (std::find(sceneCollections.begin(), sceneCollections.end(), sceneCollectionName) != sceneCollections.end())
return RequestResult::Error(RequestStatus::SceneCollectionAlreadyExists);
return RequestResult::Error(RequestStatus::ResourceAlreadyExists);
QMainWindow* mainWindow = reinterpret_cast<QMainWindow*>(obs_frontend_get_main_window());
QMainWindow* mainWindow = static_cast<QMainWindow*>(obs_frontend_get_main_window());
bool success = false;
QMetaObject::invokeMethod(mainWindow, "AddSceneCollection", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, success), Q_ARG(bool, true), Q_ARG(QString, QString::fromStdString(sceneCollectionName)));
if (!success)
return RequestResult::Error(RequestStatus::RequestProcessingFailed, "Failed to create the scene collection for an unknown reason");
return RequestResult::Error(RequestStatus::ResourceCreationFailed, "Failed to create the scene collection.");
return RequestResult::Success();
}
RequestResult RequestHandler::GetProfileList(const Request& request)
/**
* Gets an array of all profiles
*
* @responseField currentProfileName | String | The name of the current profile
* @responseField profiles | Array<String> | Array of all available profiles
*
* @requestType GetProfileList
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api requests
*/
RequestResult RequestHandler::GetProfileList(const Request&)
{
json responseData;
responseData["currentProfileName"] = Utils::Obs::StringHelper::GetCurrentProfile();
responseData["profiles"] = Utils::Obs::ListHelper::GetProfileList();
responseData["profiles"] = Utils::Obs::ArrayHelper::GetProfileList();
return RequestResult::Success(responseData);
}
/**
* Switches to a profile.
*
* @requestField profileName | String | Name of the profile to switch to
*
* @requestType SetCurrentProfile
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api requests
*/
RequestResult RequestHandler::SetCurrentProfile(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -131,21 +244,33 @@ RequestResult RequestHandler::SetCurrentProfile(const Request& request)
std::string profileName = request.RequestData["profileName"];
auto profiles = Utils::Obs::ListHelper::GetProfileList();
auto profiles = Utils::Obs::ArrayHelper::GetProfileList();
if (std::find(profiles.begin(), profiles.end(), profileName) == profiles.end())
return RequestResult::Error(RequestStatus::ProfileNotFound);
return RequestResult::Error(RequestStatus::ResourceNotFound);
std::string currentProfileName = Utils::Obs::StringHelper::GetCurrentProfile();
// Avoid queueing tasks if nothing will change
if (currentProfileName != profileName) {
obs_queue_task(OBS_TASK_UI, [](void* param) {
obs_frontend_set_current_profile(reinterpret_cast<const char*>(param));
obs_frontend_set_current_profile(static_cast<const char*>(param));
}, (void*)profileName.c_str(), true);
}
return RequestResult::Success();
}
/**
* Creates a new profile, switching to it in the process
*
* @requestField profileName | String | Name for the new profile
*
* @requestType CreateProfile
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api requests
*/
RequestResult RequestHandler::CreateProfile(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -155,16 +280,28 @@ RequestResult RequestHandler::CreateProfile(const Request& request)
std::string profileName = request.RequestData["profileName"];
auto profiles = Utils::Obs::ListHelper::GetProfileList();
auto profiles = Utils::Obs::ArrayHelper::GetProfileList();
if (std::find(profiles.begin(), profiles.end(), profileName) != profiles.end())
return RequestResult::Error(RequestStatus::ProfileAlreadyExists);
return RequestResult::Error(RequestStatus::ResourceAlreadyExists);
QMainWindow* mainWindow = reinterpret_cast<QMainWindow*>(obs_frontend_get_main_window());
QMainWindow* mainWindow = static_cast<QMainWindow*>(obs_frontend_get_main_window());
QMetaObject::invokeMethod(mainWindow, "NewProfile", Qt::BlockingQueuedConnection, Q_ARG(QString, QString::fromStdString(profileName)));
return RequestResult::Success();
}
/**
* Removes a profile. If the current profile is chosen, it will change to a different profile first.
*
* @requestField profileName | String | Name of the profile to remove
*
* @requestType RemoveProfile
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api requests
*/
RequestResult RequestHandler::RemoveProfile(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -174,19 +311,35 @@ RequestResult RequestHandler::RemoveProfile(const Request& request)
std::string profileName = request.RequestData["profileName"];
auto profiles = Utils::Obs::ListHelper::GetProfileList();
auto profiles = Utils::Obs::ArrayHelper::GetProfileList();
if (std::find(profiles.begin(), profiles.end(), profileName) == profiles.end())
return RequestResult::Error(RequestStatus::ProfileNotFound);
return RequestResult::Error(RequestStatus::ResourceNotFound);
if (profiles.size() < 2)
return RequestResult::Error(RequestStatus::NotEnoughProfiles);
return RequestResult::Error(RequestStatus::NotEnoughResources);
QMainWindow* mainWindow = reinterpret_cast<QMainWindow*>(obs_frontend_get_main_window());
QMainWindow* mainWindow = static_cast<QMainWindow*>(obs_frontend_get_main_window());
QMetaObject::invokeMethod(mainWindow, "DeleteProfile", Qt::BlockingQueuedConnection, Q_ARG(QString, QString::fromStdString(profileName)));
return RequestResult::Success();
}
/**
* Gets a parameter from the current profile's configuration.
*
* @requestField parameterCategory | String | Category of the parameter to get
* @requestField parameterName | String | Name of the parameter to get
*
* @responseField parameterValue | String | Value associated with the parameter. `null` if not set and no default
* @responseField defaultParameterValue | String | Default value associated with the parameter. `null` if no default
*
* @requestType GetProfileParameter
* @complexity 4
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api requests
*/
RequestResult RequestHandler::GetProfileParameter(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -217,6 +370,20 @@ RequestResult RequestHandler::GetProfileParameter(const Request& request)
return RequestResult::Success(responseData);
}
/**
* Sets the value of a parameter in the current profile's configuration.
*
* @requestField parameterCategory | String | Category of the parameter to set
* @requestField parameterName | String | Name of the parameter to set
* @requestField parameterValue | String | Value of the parameter to set. Use `null` to delete
*
* @requestType SetProfileParameter
* @complexity 4
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api requests
*/
RequestResult RequestHandler::SetProfileParameter(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -233,18 +400,39 @@ RequestResult RequestHandler::SetProfileParameter(const Request& request)
// Using check helpers here would just make the logic more complicated
if (!request.RequestData.contains("parameterValue") || request.RequestData["parameterValue"].is_null()) {
if (!config_remove_value(profile, parameterCategory.c_str(), parameterName.c_str()))
return RequestResult::Error(RequestStatus::ConfigParameterNotFound);
return RequestResult::Error(RequestStatus::ResourceNotFound, "There are no existing instances of that profile parameter.");
} else if (request.RequestData["parameterValue"].is_string()) {
std::string parameterValue = request.RequestData["parameterValue"];
config_set_string(profile, parameterCategory.c_str(), parameterName.c_str(), parameterValue.c_str());
} else {
return RequestResult::Error(RequestStatus::InvalidRequestParameterDataType, "The parameter `parameterValue` must be a string.");
return RequestResult::Error(RequestStatus::InvalidRequestFieldType, "The field `parameterValue` must be a string.");
}
config_save(profile);
return RequestResult::Success();
}
RequestResult RequestHandler::GetVideoSettings(const Request& request)
/**
* Gets the current video settings.
*
* Note: To get the true FPS value, divide the FPS numerator by the FPS denominator. Example: `60000/1001`
*
* @responseField fpsNumerator | Number | Numerator of the fractional FPS value
* @responseField fpsDenominator | Number | Denominator of the fractional FPS value
* @responseField baseWidth | Number | Width of the base (canvas) resolution in pixels
* @responseField baseHeight | Number | Height of the base (canvas) resolution in pixels
* @responseField outputWidth | Number | Width of the output resolution in pixels
* @responseField outputHeight | Number | Height of the output resolution in pixels
*
* @requestType GetVideoSettings
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api requests
*/
RequestResult RequestHandler::GetVideoSettings(const Request&)
{
struct obs_video_info ovi;
if (!obs_get_video_info(&ovi))
@ -261,6 +449,25 @@ RequestResult RequestHandler::GetVideoSettings(const Request& request)
return RequestResult::Success(responseData);
}
/**
* Sets the current video settings.
*
* Note: Fields must be specified in pairs. For example, you cannot set only `baseWidth` without needing to specify `baseHeight`.
*
* @requestField ?fpsNumerator | Number | Numerator of the fractional FPS value | >= 1 | Not changed
* @requestField ?fpsDenominator | Number | Denominator of the fractional FPS value | >= 1 | Not changed
* @requestField ?baseWidth | Number | Width of the base (canvas) resolution in pixels | >= 1, <= 4096 | Not changed
* @requestField ?baseHeight | Number | Height of the base (canvas) resolution in pixels | >= 1, <= 4096 | Not changed
* @requestField ?outputWidth | Number | Width of the output resolution in pixels | >= 1, <= 4096 | Not changed
* @requestField ?outputHeight | Number | Height of the output resolution in pixels | >= 1, <= 4096 | Not changed
*
* @requestType SetVideoSettings
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api requests
*/
RequestResult RequestHandler::SetVideoSettings(const Request& request)
{
if (obs_video_active())
@ -268,16 +475,16 @@ RequestResult RequestHandler::SetVideoSettings(const Request& request)
RequestStatus::RequestStatus statusCode = RequestStatus::NoError;
std::string comment;
bool changeFps = (request.ValidateNumber("fpsNumerator", statusCode, comment, 1) && request.ValidateNumber("fpsDenominator", statusCode, comment, 1));
if (!changeFps && statusCode != RequestStatus::MissingRequestParameter)
bool changeFps = (request.Contains("fpsNumerator") && request.Contains("fpsDenominator"));
if (changeFps && !(request.ValidateOptionalNumber("fpsNumerator", statusCode, comment, 1) && request.ValidateOptionalNumber("fpsDenominator", statusCode, comment, 1)))
return RequestResult::Error(statusCode, comment);
bool changeBaseRes = (request.ValidateNumber("baseWidth", statusCode, comment, 8, 4096) && request.ValidateNumber("baseHeight", statusCode, comment, 8, 4096));
if (!changeBaseRes && statusCode != RequestStatus::MissingRequestParameter)
bool changeBaseRes = (request.Contains("baseWidth") && request.Contains("baseHeight"));
if (changeBaseRes && !(request.ValidateOptionalNumber("baseWidth", statusCode, comment, 8, 4096) && request.ValidateOptionalNumber("baseHeight", statusCode, comment, 8, 4096)))
return RequestResult::Error(statusCode, comment);
bool changeOutputRes = (request.ValidateNumber("outputWidth", statusCode, comment, 8, 4096) && request.ValidateNumber("outputHeight", statusCode, comment, 8, 4096));
if (!changeOutputRes && statusCode != RequestStatus::MissingRequestParameter)
bool changeOutputRes = (request.Contains("outputWidth") && request.Contains("outputHeight"));
if (changeOutputRes && !(request.ValidateOptionalNumber("outputWidth", statusCode, comment, 8, 4096) && request.ValidateOptionalNumber("outputHeight", statusCode, comment, 8, 4096)))
return RequestResult::Error(statusCode, comment);
config_t *config = obs_frontend_get_profile_config();
@ -304,10 +511,23 @@ RequestResult RequestHandler::SetVideoSettings(const Request& request)
return RequestResult::Success();
}
return RequestResult::Error(RequestStatus::MissingRequestParameter, "You must specify at least one video-changing pair.");
return RequestResult::Error(RequestStatus::MissingRequestField, "You must specify at least one video-changing pair.");
}
RequestResult RequestHandler::GetStreamServiceSettings(const Request& request)
/**
* Gets the current stream service settings (stream destination).
*
* @responseField streamServiceType | String | Stream service type, like `rtmp_custom` or `rtmp_common`
* @responseField streamServiceSettings | Object | Stream service settings
*
* @requestType GetStreamServiceSettings
* @complexity 4
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api requests
*/
RequestResult RequestHandler::GetStreamServiceSettings(const Request&)
{
json responseData;
@ -319,10 +539,25 @@ RequestResult RequestHandler::GetStreamServiceSettings(const Request& request)
return RequestResult::Success(responseData);
}
/**
* Sets the current stream service settings (stream destination).
*
* Note: Simple RTMP settings can be set with type `rtmp_custom` and the settings fields `server` and `key`.
*
* @requestField streamServiceType | String | Type of stream service to apply. Example: `rtmp_common` or `rtmp_custom`
* @requestField streamServiceSettings | Object | Settings to apply to the service
*
* @requestType SetStreamServiceSettings
* @complexity 4
* @rpcVersion -1
* @initialVersion 5.0.0
* @category config
* @api requests
*/
RequestResult RequestHandler::SetStreamServiceSettings(const Request& request)
{
if (obs_frontend_streaming_active())
return RequestResult::Error(RequestStatus::StreamRunning);
return RequestResult::Error(RequestStatus::OutputRunning, "You cannot change stream service settings while streaming.");
RequestStatus::RequestStatus statusCode;
std::string comment;
@ -347,10 +582,10 @@ RequestResult RequestHandler::SetStreamServiceSettings(const Request& request)
obs_service_update(currentStreamService, newStreamServiceSettings);
} else {
// TODO: This leaks memory. I have no idea why.
OBSService newStreamService = obs_service_create(requestedStreamServiceType.c_str(), "obs_websocket_custom_service", requestedStreamServiceSettings, NULL);
OBSService newStreamService = obs_service_create(requestedStreamServiceType.c_str(), "obs_websocket_custom_service", requestedStreamServiceSettings, nullptr);
// TODO: Check service type here, instead of relying on service creation to fail.
if (!newStreamService)
return RequestResult::Error(RequestStatus::StreamServiceCreationFailed, "Creating the stream service with the requested streamServiceType failed. It may be an invalid type.");
return RequestResult::Error(RequestStatus::ResourceCreationFailed, "Failed to create the stream service with the requested streamServiceType. It may be an invalid type.");
obs_frontend_set_streaming_service(newStreamService);
}
@ -359,3 +594,23 @@ RequestResult RequestHandler::SetStreamServiceSettings(const Request& request)
return RequestResult::Success();
}
/**
* Gets the current directory that the record output is set to.
*
* @responseField recordDirectory | String | Output directory
*
* @requestType GetRecordDirectory
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category rconfig
*/
RequestResult RequestHandler::GetRecordDirectory(const Request&)
{
json responseData;
responseData["recordDirectory"] = Utils::Obs::StringHelper::GetCurrentRecordOutputPath();
return RequestResult::Success(responseData);
}

View File

@ -0,0 +1,334 @@
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#include "RequestHandler.h"
/**
* Gets an array of all of a source's filters.
*
* @requestField sourceName | String | Name of the source
*
* @responseField filters | Array<Object> | Array of filters
*
* @requestType GetSourceFilterList
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category filters
*/
RequestResult RequestHandler::GetSourceFilterList(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSourceAutoRelease source = request.ValidateSource("sourceName", statusCode, comment);
if(!source)
return RequestResult::Error(statusCode, comment);
json responseData;
responseData["filters"] = Utils::Obs::ArrayHelper::GetSourceFilterList(source);
return RequestResult::Success(responseData);
}
/**
* Gets the default settings for a filter kind.
*
* @requestField filterKind | String | Filter kind to get the default settings for
*
* @responseField defaultFilterSettings | Object | Object of default settings for the filter kind
*
* @requestType GetSourceFilterDefaultSettings
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category filters
*/
RequestResult RequestHandler::GetSourceFilterDefaultSettings(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
if (!request.ValidateString("filterKind", statusCode, comment))
return RequestResult::Error(statusCode, comment);
std::string filterKind = request.RequestData["filterKind"];
auto kinds = Utils::Obs::ArrayHelper::GetFilterKindList();
if (std::find(kinds.begin(), kinds.end(), filterKind) == kinds.end())
return RequestResult::Error(RequestStatus::InvalidFilterKind);
OBSDataAutoRelease defaultSettings = obs_get_source_defaults(filterKind.c_str());
if (!defaultSettings)
return RequestResult::Error(RequestStatus::InvalidFilterKind);
json responseData;
responseData["defaultFilterSettings"] = Utils::Json::ObsDataToJson(defaultSettings, true);
return RequestResult::Success(responseData);
}
/**
* Creates a new filter, adding it to the specified source.
*
* @requestField sourceName | String | Name of the source to add the filter to
* @requestField filterName | String | Name of the new filter to be created
* @requestField filterKind | String | The kind of filter to be created
* @requestField ?filterSettings | Object | Settings object to initialize the filter with | Default settings used
*
* @requestType CreateSourceFilter
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category filters
*/
RequestResult RequestHandler::CreateSourceFilter(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSourceAutoRelease source = request.ValidateSource("sourceName", statusCode, comment);
if (!(source && request.ValidateString("filterName", statusCode, comment) && request.ValidateString("filterKind", statusCode, comment)))
return RequestResult::Error(statusCode, comment);
std::string filterName = request.RequestData["filterName"];
OBSSourceAutoRelease existingFilter = obs_source_get_filter_by_name(source, filterName.c_str());
if (existingFilter)
return RequestResult::Error(RequestStatus::ResourceAlreadyExists, "A filter already exists by that name.");
std::string filterKind = request.RequestData["filterKind"];
auto kinds = Utils::Obs::ArrayHelper::GetFilterKindList();
if (std::find(kinds.begin(), kinds.end(), filterKind) == kinds.end())
return RequestResult::Error(RequestStatus::InvalidFilterKind, "Your specified filter kind is not supported by OBS. Check that any necessary plugins are loaded.");
OBSDataAutoRelease filterSettings = nullptr;
if (request.Contains("filterSettings")) {
if (!request.ValidateOptionalObject("filterSettings", statusCode, comment, true))
return RequestResult::Error(statusCode, comment);
filterSettings = Utils::Json::JsonToObsData(request.RequestData["filterSettings"]);
}
OBSSourceAutoRelease filter = Utils::Obs::ActionHelper::CreateSourceFilter(source, filterName, filterKind, filterSettings);
if(!filter)
return RequestResult::Error(RequestStatus::ResourceCreationFailed, "Creation of the filter failed.");
return RequestResult::Success();
}
/**
* Removes a filter from a source.
*
* @requestField sourceName | String | Name of the source the filter is on
* @requestField filterName | String | Name of the filter to remove
*
* @requestType RemoveSourceFilter
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category filters
*/
RequestResult RequestHandler::RemoveSourceFilter(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
FilterPair pair = request.ValidateFilter("sourceName", "filterName", statusCode, comment);
if (!pair.filter)
return RequestResult::Error(statusCode, comment);
obs_source_filter_remove(pair.source, pair.filter);
return RequestResult::Success();
}
/**
* Sets the name of a source filter (rename).
*
* @requestField sourceName | String | Name of the source the filter is on
* @requestField filterName | String | Current name of the filter
* @requestField newFilterName | String | New name for the filter
*
* @requestType SetSourceFilterName
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category filters
*/
RequestResult RequestHandler::SetSourceFilterName(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
FilterPair pair = request.ValidateFilter("sourceName", "filterName", statusCode, comment);
if (!pair.filter || !request.ValidateString("newFilterName", statusCode, comment))
return RequestResult::Error(statusCode, comment);
std::string newFilterName = request.RequestData["newFilterName"];
OBSSourceAutoRelease existingFilter = obs_source_get_filter_by_name(pair.source, newFilterName.c_str());
if (existingFilter)
return RequestResult::Error(RequestStatus::ResourceAlreadyExists, "A filter already exists by that new name.");
obs_source_set_name(pair.filter, newFilterName.c_str());
return RequestResult::Success();
}
/**
* Gets the info for a specific source filter.
*
* @requestField sourceName | String | Name of the source
* @requestField filterName | String | Name of the filter
*
* @responseField filterEnabled | Boolean | Whether the filter is enabled
* @responseField filterIndex | Number | Index of the filter in the list, beginning at 0
* @responseField filterKind | String | The kind of filter
* @responseField filterSettings | Object | Settings object associated with the filter
*
* @requestType GetSourceFilter
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category filters
*/
RequestResult RequestHandler::GetSourceFilter(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
FilterPair pair = request.ValidateFilter("sourceName", "filterName", statusCode, comment);
if (!pair.filter)
return RequestResult::Error(statusCode, comment);
json responseData;
responseData["filterEnabled"] = obs_source_enabled(pair.filter);
responseData["filterIndex"] = Utils::Obs::NumberHelper::GetSourceFilterIndex(pair.source, pair.filter); // Todo: Use `GetSourceFilterlist` to select this filter maybe
responseData["filterKind"] = obs_source_get_id(pair.filter);
OBSDataAutoRelease filterSettings = obs_source_get_settings(pair.filter);
responseData["filterSettings"] = Utils::Json::ObsDataToJson(filterSettings);
return RequestResult::Success(responseData);
}
/**
* Sets the index position of a filter on a source.
*
* @requestField sourceName | String | Name of the source the filter is on
* @requestField filterName | String | Name of the filter
* @requestField filterIndex | Number | New index position of the filter | >= 0
*
* @requestType SetSourceFilterIndex
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category filters
*/
RequestResult RequestHandler::SetSourceFilterIndex(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
FilterPair pair = request.ValidateFilter("sourceName", "filterName", statusCode, comment);
if (!(pair.filter && request.ValidateNumber("filterIndex", statusCode, comment, 0, 8192)))
return RequestResult::Error(statusCode, comment);
int filterIndex = request.RequestData["filterIndex"];
Utils::Obs::ActionHelper::SetSourceFilterIndex(pair.source, pair.filter, filterIndex);
return RequestResult::Success();
}
/**
* Sets the settings of a source filter.
*
* @requestField sourceName | String | Name of the source the filter is on
* @requestField filterName | String | Name of the filter to set the settings of
* @requestField filterSettings | Object | Object of settings to apply
* @requestField ?overlay | Boolean | True == apply the settings on top of existing ones, False == reset the input to its defaults, then apply settings. | true
*
* @requestType SetSourceFilterSettings
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category filters
*/
RequestResult RequestHandler::SetSourceFilterSettings(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
FilterPair pair = request.ValidateFilter("sourceName", "filterName", statusCode, comment);
if (!(pair.filter && request.ValidateObject("filterSettings", statusCode, comment, true)))
return RequestResult::Error(statusCode, comment);
// Almost identical to SetInputSettings
bool overlay = true;
if (request.Contains("overlay")) {
if (!request.ValidateOptionalBoolean("overlay", statusCode, comment))
return RequestResult::Error(statusCode, comment);
overlay = request.RequestData["overlay"];
}
OBSDataAutoRelease newSettings = Utils::Json::JsonToObsData(request.RequestData["filterSettings"]);
if (!newSettings)
return RequestResult::Error(RequestStatus::RequestProcessingFailed, "An internal data conversion operation failed. Please report this!");
if (overlay)
obs_source_update(pair.filter, newSettings);
else
obs_source_reset_settings(pair.filter, newSettings);
obs_source_update_properties(pair.filter);
return RequestResult::Success();
}
/**
* Sets the enable state of a source filter.
*
* @requestField sourceName | String | Name of the source the filter is on
* @requestField filterName | Number | Name of the filter
* @requestField filterEnabled | Boolean | New enable state of the filter
*
* @requestType SetSourceFilterEnabled
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category filters
*/
RequestResult RequestHandler::SetSourceFilterEnabled(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
FilterPair pair = request.ValidateFilter("sourceName", "filterName", statusCode, comment);
if (!(pair.filter && request.ValidateBoolean("filterEnabled", statusCode, comment)))
return RequestResult::Error(statusCode, comment);
bool filterEnabled = request.RequestData["filterEnabled"];
obs_source_set_enabled(pair.filter, filterEnabled);
return RequestResult::Success();
}

View File

@ -1,15 +1,51 @@
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#include <QImageWriter>
#include "RequestHandler.h"
#include "../websocketserver/WebSocketServer.h"
#include "../eventhandler/types/EventSubscription.h"
#include "../WebSocketApi.h"
#include "../obs-websocket.h"
#include "../WebSocketServer.h"
#include "../plugin-macros.generated.h"
RequestResult RequestHandler::GetVersion(const Request& request)
/**
* Gets data about the current plugin and RPC version.
*
* @responseField obsVersion | String | Current OBS Studio version
* @responseField obsWebSocketVersion | String | Current obs-websocket version
* @responseField rpcVersion | Number | Current latest obs-websocket RPC version
* @responseField availableRequests | Array<String> | Array of available RPC requests for the currently negotiated RPC version
* @responseField supportedImageFormats | Array<String> | Image formats available in `GetSourceScreenshot` and `SaveSourceScreenshot` requests.
*
* @requestType GetVersion
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category general
* @api requests
*/
RequestResult RequestHandler::GetVersion(const Request&)
{
json responseData;
responseData["obsVersion"] = Utils::Obs::StringHelper::GetObsVersionString();
responseData["obsVersion"] = Utils::Obs::StringHelper::GetObsVersion();
responseData["obsWebSocketVersion"] = OBS_WEBSOCKET_VERSION;
responseData["rpcVersion"] = OBS_WEBSOCKET_RPC_VERSION;
responseData["availableRequests"] = GetRequestList();
@ -24,6 +60,50 @@ RequestResult RequestHandler::GetVersion(const Request& request)
return RequestResult::Success(responseData);
}
/**
* Gets statistics about OBS, obs-websocket, and the current session.
*
* @responseField cpuUsage | Number | Current CPU usage in percent
* @responseField memoryUsage | Number | Amount of memory in MB currently being used by OBS
* @responseField availableDiskSpace | Number | Available disk space on the device being used for recording storage
* @responseField activeFps | Number | Current FPS being rendered
* @responseField averageFrameRenderTime | Number | Average time in milliseconds that OBS is taking to render a frame
* @responseField renderSkippedFrames | Number | Number of frames skipped by OBS in the render thread
* @responseField renderTotalFrames | Number | Total number of frames outputted by the render thread
* @responseField outputSkippedFrames | Number | Number of frames skipped by OBS in the output thread
* @responseField outputTotalFrames | Number | Total number of frames outputted by the output thread
* @responseField webSocketSessionIncomingMessages | Number | Total number of messages received by obs-websocket from the client
* @responseField webSocketSessionOutgoingMessages | Number | Total number of messages sent by obs-websocket to the client
*
* @requestType GetStats
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @category general
* @api requests
*/
RequestResult RequestHandler::GetStats(const Request&)
{
json responseData = Utils::Obs::ObjectHelper::GetStats();
responseData["webSocketSessionIncomingMessages"] = _session->IncomingMessages();
responseData["webSocketSessionOutgoingMessages"] = _session->OutgoingMessages();
return RequestResult::Success(responseData);
}
/**
* Broadcasts a `CustomEvent` to all WebSocket clients. Receivers are clients which are identified and subscribed.
*
* @requestField eventData | Object | Data payload to emit to all receivers
*
* @requestType BroadcastCustomEvent
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category general
* @api requests
*/
RequestResult RequestHandler::BroadcastCustomEvent(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -33,30 +113,104 @@ RequestResult RequestHandler::BroadcastCustomEvent(const Request& request)
auto webSocketServer = GetWebSocketServer();
if (!webSocketServer)
return RequestResult::Error(RequestStatus::RequestProcessingFailed, "Unable to send event.");
return RequestResult::Error(RequestStatus::RequestProcessingFailed, "Unable to send event due to internal error.");
webSocketServer->BroadcastEvent(EventSubscription::General, "CustomEvent", request.RequestData["eventData"]);
return RequestResult::Success();
}
RequestResult RequestHandler::GetStats(const Request& request)
/**
* Call a request registered to a vendor.
*
* A vendor is a unique name registered by a third-party plugin or script, which allows for custom requests and events to be added to obs-websocket.
* If a plugin or script implements vendor requests or events, documentation is expected to be provided with them.
*
* @requestField vendorName | String | Name of the vendor to use
* @requestField requestType | String | The request type to call
* @requestField ?requestData | Object | Object containing appropriate request data | {}
*
* @responseField responseData | Object | Object containing appropriate response data. {} if request does not provide any response data
*
* @requestType CallVendorRequest
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @category general
* @api requests
*/
RequestResult RequestHandler::CallVendorRequest(const Request& request)
{
json responseData = Utils::Obs::DataHelper::GetStats();
RequestStatus::RequestStatus statusCode;
std::string comment;
if (!request.ValidateString("vendorName", statusCode, comment) || !request.ValidateString("requestType", statusCode, comment))
return RequestResult::Error(statusCode, comment);
responseData["webSocketSessionIncomingMessages"] = request.Session->IncomingMessages();
responseData["webSocketSessionOutgoingMessages"] = request.Session->OutgoingMessages();
std::string vendorName = request.RequestData["vendorName"];
std::string requestType = request.RequestData["requestType"];
OBSDataAutoRelease requestData = obs_data_create();
if (request.Contains("requestData")) {
if (!request.ValidateOptionalObject("requestData", statusCode, comment))
return RequestResult::Error(statusCode, comment);
requestData = Utils::Json::JsonToObsData(request.RequestData["requestData"]);
}
OBSDataAutoRelease obsResponseData = obs_data_create();
auto webSocketApi = GetWebSocketApi();
if (!webSocketApi)
return RequestResult::Error(RequestStatus::RequestProcessingFailed, "Unable to call request due to internal error.");
auto ret = webSocketApi->PerformVendorRequest(vendorName, requestType, requestData, obsResponseData);
switch (ret) {
default:
case WebSocketApi::RequestReturnCode::Normal:
break;
case WebSocketApi::RequestReturnCode::NoVendor:
return RequestResult::Error(RequestStatus::ResourceNotFound, "No vendor was found by that name.");
case WebSocketApi::RequestReturnCode::NoVendorRequest:
return RequestResult::Error(RequestStatus::ResourceNotFound, "No request was found by that name.");
}
json responseData;
responseData["responseData"] = Utils::Json::ObsDataToJson(obsResponseData);
return RequestResult::Success(responseData);
}
RequestResult RequestHandler::GetHotkeyList(const Request& request)
/**
* Gets an array of all hotkey names in OBS
*
* @responseField hotkeys | Array<String> | Array of hotkey names
*
* @requestType GetHotkeyList
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @category general
* @api requests
*/
RequestResult RequestHandler::GetHotkeyList(const Request&)
{
json responseData;
responseData["hotkeys"] = Utils::Obs::ListHelper::GetHotkeyNameList();
responseData["hotkeys"] = Utils::Obs::ArrayHelper::GetHotkeyNameList();
return RequestResult::Success(responseData);
}
/**
* Triggers a hotkey using its name. See `GetHotkeyList`
*
* @requestField hotkeyName | String | Name of the hotkey to trigger
*
* @requestType TriggerHotkeyByName
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @category general
* @api requests
*/
RequestResult RequestHandler::TriggerHotkeyByName(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -66,13 +220,30 @@ RequestResult RequestHandler::TriggerHotkeyByName(const Request& request)
obs_hotkey_t *hotkey = Utils::Obs::SearchHelper::GetHotkeyByName(request.RequestData["hotkeyName"]);
if (!hotkey)
return RequestResult::Error(RequestStatus::HotkeyNotFound);
return RequestResult::Error(RequestStatus::ResourceNotFound, "No hotkeys were found by that name.");
obs_hotkey_trigger_routed_callback(obs_hotkey_get_id(hotkey), true);
return RequestResult::Success();
}
/**
* Triggers a hotkey using a sequence of keys.
*
* @requestField ?keyId | String | The OBS key ID to use. See https://github.com/obsproject/obs-studio/blob/master/libobs/obs-hotkeys.h | Not pressed
* @requestField ?keyModifiers | Object | Object containing key modifiers to apply | Ignored
* @requestField ?keyModifiers.shift | Boolean | Press Shift | Not pressed
* @requestField ?keyModifiers.control | Boolean | Press CTRL | Not pressed
* @requestField ?keyModifiers.alt | Boolean | Press ALT | Not pressed
* @requestField ?keyModifiers.command | Boolean | Press CMD (Mac) | Not pressed
*
* @requestType TriggerHotkeyByKeySequence
* @complexity 4
* @rpcVersion -1
* @initialVersion 5.0.0
* @category general
* @api requests
*/
RequestResult RequestHandler::TriggerHotkeyByKeySequence(const Request& request)
{
obs_key_combination_t combo = {0};
@ -80,8 +251,8 @@ RequestResult RequestHandler::TriggerHotkeyByKeySequence(const Request& request)
RequestStatus::RequestStatus statusCode = RequestStatus::NoError;
std::string comment;
if (request.RequestData.contains("keyId") && !request.RequestData["keyId"].is_null()) {
if (!request.ValidateString("keyId", statusCode, comment))
if (request.Contains("keyId")) {
if (!request.ValidateOptionalString("keyId", statusCode, comment))
return RequestResult::Error(statusCode, comment);
std::string keyId = request.RequestData["keyId"];
@ -89,8 +260,8 @@ RequestResult RequestHandler::TriggerHotkeyByKeySequence(const Request& request)
}
statusCode = RequestStatus::NoError;
if (request.RequestData.contains("keyModifiers") && !request.RequestData["keyModifiers"].is_null()) {
if (!request.ValidateObject("keyModifiers", statusCode, comment, true))
if (request.Contains("keyModifiers")) {
if (!request.ValidateOptionalObject("keyModifiers", statusCode, comment, true))
return RequestResult::Error(statusCode, comment);
const json keyModifiersJson = request.RequestData["keyModifiers"];
@ -107,7 +278,7 @@ RequestResult RequestHandler::TriggerHotkeyByKeySequence(const Request& request)
}
if (!combo.modifiers && (combo.key == OBS_KEY_NONE || combo.key >= OBS_KEY_LAST_VALUE))
return RequestResult::Error(RequestStatus::CannotAct, "Your provided request parameters cannot be used to trigger a hotkey.");
return RequestResult::Error(RequestStatus::CannotAct, "Your provided request fields cannot be used to trigger a hotkey.");
// Apparently things break when you don't start by setting the combo to false
obs_hotkey_inject_event(combo, false);
@ -117,43 +288,37 @@ RequestResult RequestHandler::TriggerHotkeyByKeySequence(const Request& request)
return RequestResult::Success();
}
RequestResult RequestHandler::GetStudioModeEnabled(const Request& request)
{
json responseData;
responseData["studioModeEnabled"] = obs_frontend_preview_program_mode_active();
return RequestResult::Success(responseData);
}
RequestResult RequestHandler::SetStudioModeEnabled(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
if (!request.ValidateBoolean("studioModeEnabled", statusCode, comment))
return RequestResult::Error(statusCode, comment);
// Avoid queueing tasks if nothing will change
if (obs_frontend_preview_program_mode_active() != request.RequestData["studioModeEnabled"]) {
// (Bad) Create a boolean then pass it as a reference to the task. Requires `wait` in obs_queue_task() to be true, else undefined behavior
bool studioModeEnabled = request.RequestData["studioModeEnabled"];
// Queue the task inside of the UI thread to prevent race conditions
obs_queue_task(OBS_TASK_UI, [](void* param) {
auto studioModeEnabled = (bool*)param;
obs_frontend_set_preview_program_mode(*studioModeEnabled);
}, &studioModeEnabled, true);
}
return RequestResult::Success();
}
/**
* Sleeps for a time duration or number of frames. Only available in request batches with types `SERIAL_REALTIME` or `SERIAL_FRAME`.
*
* @requestField sleepMillis | Number | Number of milliseconds to sleep for (if `SERIAL_REALTIME` mode) | >= 0, <= 50000
* @requestField sleepFrames | Number | Number of frames to sleep for (if `SERIAL_FRAME` mode) | >= 0, <= 10000
*
* @requestType Sleep
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @category general
* @api requests
*/
RequestResult RequestHandler::Sleep(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
if (!request.ValidateNumber("sleepMillis", statusCode, comment, 0, 50000))
return RequestResult::Error(statusCode, comment);
int64_t sleepMillis = request.RequestData["sleepMillis"];
std::this_thread::sleep_for(std::chrono::milliseconds(sleepMillis));
return RequestResult::Success();
if (request.ExecutionType == RequestBatchExecutionType::SerialRealtime) {
if (!request.ValidateNumber("sleepMillis", statusCode, comment, 0, 50000))
return RequestResult::Error(statusCode, comment);
int64_t sleepMillis = request.RequestData["sleepMillis"];
std::this_thread::sleep_for(std::chrono::milliseconds(sleepMillis));
return RequestResult::Success();
} else if (request.ExecutionType == RequestBatchExecutionType::SerialFrame) {
if (!request.ValidateNumber("sleepFrames", statusCode, comment, 0, 10000))
return RequestResult::Error(statusCode, comment);
RequestResult ret = RequestResult::Success();
ret.SleepFrames = request.RequestData["sleepFrames"];
return ret;
} else {
return RequestResult::Error(RequestStatus::UnsupportedRequestBatchExecutionType);
}
}

View File

@ -1,66 +1,164 @@
#include "RequestHandler.h"
#include "../plugin-macros.generated.h"
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#include "RequestHandler.h"
/**
* Gets an array of all inputs in OBS.
*
* @requestField ?inputKind | String | Restrict the array to only inputs of the specified kind | All kinds included
*
* @responseField inputs | Array<Object> | Array of inputs
*
* @requestType GetInputList
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::GetInputList(const Request& request)
{
std::string inputKind;
if (request.RequestData.contains("inputKind") && !request.RequestData["inputKind"].is_null()) {
if (request.Contains("inputKind")) {
RequestStatus::RequestStatus statusCode;
std::string comment;
if (!request.ValidateString("inputKind", statusCode, comment))
if (!request.ValidateOptionalString("inputKind", statusCode, comment))
return RequestResult::Error(statusCode, comment);
inputKind = request.RequestData["inputKind"];
}
json responseData;
responseData["inputs"] = Utils::Obs::ListHelper::GetInputList(inputKind);
responseData["inputs"] = Utils::Obs::ArrayHelper::GetInputList(inputKind);
return RequestResult::Success(responseData);
}
/**
* Gets an array of all available input kinds in OBS.
*
* @requestField ?unversioned | Boolean | True == Return all kinds as unversioned, False == Return with version suffixes (if available) | false
*
* @responseField inputKinds | Array<String> | Array of input kinds
*
* @requestType GetInputKindList
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::GetInputKindList(const Request& request)
{
bool unversioned = false;
if (request.RequestData.contains("unversioned") && !request.RequestData["unversioned"].is_null()) {
if (request.Contains("unversioned")) {
RequestStatus::RequestStatus statusCode;
std::string comment;
if (!request.ValidateBoolean("unversioned", statusCode, comment))
if (!request.ValidateOptionalBoolean("unversioned", statusCode, comment))
return RequestResult::Error(statusCode, comment);
unversioned = request.RequestData["unversioned"];
}
json responseData;
responseData["inputKinds"] = Utils::Obs::ListHelper::GetInputKindList(unversioned);
responseData["inputKinds"] = Utils::Obs::ArrayHelper::GetInputKindList(unversioned);
return RequestResult::Success(responseData);
}
/**
* Gets the names of all special inputs.
*
* @responseField desktop1 | String | Name of the Desktop Audio input
* @responseField desktop2 | String | Name of the Desktop Audio 2 input
* @responseField mic1 | String | Name of the Mic/Auxiliary Audio input
* @responseField mic2 | String | Name of the Mic/Auxiliary Audio 2 input
* @responseField mic3 | String | Name of the Mic/Auxiliary Audio 3 input
* @responseField mic4 | String | Name of the Mic/Auxiliary Audio 4 input
*
* @requestType GetSpecialInputs
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::GetSpecialInputs(const Request&)
{
json responseData;
std::vector<std::string> channels = {"desktop1", "desktop2", "mic1", "mic2", "mic3", "mic4"};
size_t channelId = 1;
for (auto &channel : channels) {
OBSSourceAutoRelease input = obs_get_output_source(channelId);
if (!input)
responseData[channel] = nullptr;
else
responseData[channel] = obs_source_get_name(input);
channelId++;
}
return RequestResult::Success(responseData);
}
/**
* Creates a new input, adding it as a scene item to the specified scene.
*
* @requestField sceneName | String | Name of the scene to add the input to as a scene item
* @requestField inputName | String | Name of the new input to created
* @requestField inputKind | String | The kind of input to be created
* @requestField ?inputSettings | Object | Settings object to initialize the input with | Default settings used
* @requestField ?sceneItemEnabled | Boolean | Whether to set the created scene item to enabled or disabled | True
*
* @responseField sceneItemId | Number | ID of the newly created scene item
*
* @requestType CreateInput
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::CreateInput(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSourceAutoRelease sceneSource = request.ValidateScene("sceneName", statusCode, comment);
if (!(request.ValidateString("inputName", statusCode, comment) &&
request.ValidateString("inputKind", statusCode, comment) &&
sceneSource))
if (!(sceneSource && request.ValidateString("inputName", statusCode, comment) && request.ValidateString("inputKind", statusCode, comment)))
return RequestResult::Error(statusCode, comment);
std::string inputName = request.RequestData["inputName"];
OBSSourceAutoRelease existingInput = obs_get_source_by_name(inputName.c_str());
if (existingInput)
return RequestResult::Error(RequestStatus::SourceAlreadyExists, "A source already exists by that input name.");
return RequestResult::Error(RequestStatus::ResourceAlreadyExists, "A source already exists by that input name.");
std::string inputKind = request.RequestData["inputKind"];
auto kinds = Utils::Obs::ListHelper::GetInputKindList();
auto kinds = Utils::Obs::ArrayHelper::GetInputKindList();
if (std::find(kinds.begin(), kinds.end(), inputKind) == kinds.end())
return RequestResult::Error(RequestStatus::InvalidInputKind, "Your specified input kind is not supported by OBS. Check that your specified kind is properly versioned and that any necessary plugins are loaded.");
OBSDataAutoRelease inputSettings = nullptr;
if (request.RequestData.contains("inputSettings") && !request.RequestData["inputSettings"].is_null()) {
if (!request.ValidateObject("inputSettings", statusCode, comment, true))
if (request.Contains("inputSettings")) {
if (!request.ValidateOptionalObject("inputSettings", statusCode, comment, true))
return RequestResult::Error(statusCode, comment);
inputSettings = Utils::Json::JsonToObsData(request.RequestData["inputSettings"]);
@ -69,24 +167,67 @@ RequestResult RequestHandler::CreateInput(const Request& request)
OBSScene scene = obs_scene_from_source(sceneSource);
bool sceneItemEnabled = true;
if (request.RequestData.contains("sceneItemEnabled") && !request.RequestData["sceneItemEnabled"].is_null()) {
if (!request.ValidateBoolean("sceneItemEnabled", statusCode, comment))
if (request.Contains("sceneItemEnabled")) {
if (!request.ValidateOptionalBoolean("sceneItemEnabled", statusCode, comment))
return RequestResult::Error(statusCode, comment);
sceneItemEnabled = request.RequestData["sceneItemEnabled"];
}
// Create the input and add it as a scene item to the destination scene
obs_sceneitem_t *sceneItem = Utils::Obs::ActionHelper::CreateInput(inputName, inputKind, inputSettings, scene, sceneItemEnabled);
OBSSceneItemAutoRelease sceneItem = Utils::Obs::ActionHelper::CreateInput(inputName, inputKind, inputSettings, scene, sceneItemEnabled);
if (!sceneItem)
return RequestResult::Error(RequestStatus::RequestProcessingFailed, "Creation of the input or scene item failed.");
return RequestResult::Error(RequestStatus::ResourceCreationFailed, "Creation of the input or scene item failed.");
json responseData;
responseData["sceneItemId"] = obs_sceneitem_get_id(sceneItem);
return RequestResult::Success(responseData);
}
/**
* Removes an existing input.
*
* Note: Will immediately remove all associated scene items.
*
* @requestField inputName | String | Name of the input to remove
*
* @requestType RemoveInput
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::RemoveInput(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSourceAutoRelease input = request.ValidateInput("inputName", statusCode, comment);
if (!input)
return RequestResult::Error(statusCode, comment);
// Some implementations of removing sources release before remove, and some release after.
// Releasing afterwards guarantees that we don't accidentally destroy the source before
// remove if we happen to hold the last ref (very, very rare)
obs_source_remove(input);
return RequestResult::Success();
}
/**
* Sets the name of an input (rename).
*
* @requestField inputName | String | Current input name
* @requestField newInputName | String | New name for the input
*
* @requestType SetInputName
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::SetInputName(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -99,13 +240,27 @@ RequestResult RequestHandler::SetInputName(const Request& request)
OBSSourceAutoRelease existingSource = obs_get_source_by_name(newInputName.c_str());
if (existingSource)
return RequestResult::Error(RequestStatus::SourceAlreadyExists, "A source already exists by that new input name.");
return RequestResult::Error(RequestStatus::ResourceAlreadyExists, "A source already exists by that new input name.");
obs_source_set_name(input, newInputName.c_str());
return RequestResult::Success();
}
/**
* Gets the default settings for an input kind.
*
* @requestField inputKind | String | Input kind to get the default settings for
*
* @responseField defaultInputSettings | Object | Object of default settings for the input kind
*
* @requestType GetInputDefaultSettings
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::GetInputDefaultSettings(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -114,6 +269,9 @@ RequestResult RequestHandler::GetInputDefaultSettings(const Request& request)
return RequestResult::Error(statusCode, comment);
std::string inputKind = request.RequestData["inputKind"];
auto kinds = Utils::Obs::ArrayHelper::GetInputKindList();
if (std::find(kinds.begin(), kinds.end(), inputKind) == kinds.end())
return RequestResult::Error(RequestStatus::InvalidInputKind);
OBSDataAutoRelease defaultSettings = obs_get_source_defaults(inputKind.c_str());
if (!defaultSettings)
@ -124,6 +282,23 @@ RequestResult RequestHandler::GetInputDefaultSettings(const Request& request)
return RequestResult::Success(responseData);
}
/**
* Gets the settings of an input.
*
* Note: Does not include defaults. To create the entire settings object, overlay `inputSettings` over the `defaultInputSettings` provided by `GetInputDefaultSettings`.
*
* @requestField inputName | String | Name of the input to get the settings of
*
* @responseField inputSettings | Object | Object of settings for the input
* @responseField inputKind | String | The kind of the input
*
* @requestType GetInputSettings
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::GetInputSettings(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -140,6 +315,20 @@ RequestResult RequestHandler::GetInputSettings(const Request& request)
return RequestResult::Success(responseData);
}
/**
* Sets the settings of an input.
*
* @requestField inputName | String | Name of the input to set the settings of
* @requestField inputSettings | Object | Object of settings to apply
* @requestField ?overlay | Boolean | True == apply the settings on top of existing ones, False == reset the input to its defaults, then apply settings. | true
*
* @requestType SetInputSettings
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::SetInputSettings(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -149,8 +338,8 @@ RequestResult RequestHandler::SetInputSettings(const Request& request)
return RequestResult::Error(statusCode, comment);
bool overlay = true;
if (request.RequestData.contains("overlay") && !request.RequestData["overlay"].is_null()) {
if (!request.ValidateBoolean("overlay", statusCode, comment))
if (request.Contains("overlay")) {
if (!request.ValidateOptionalBoolean("overlay", statusCode, comment))
return RequestResult::Error(statusCode, comment);
overlay = request.RequestData["overlay"];
@ -175,6 +364,20 @@ RequestResult RequestHandler::SetInputSettings(const Request& request)
return RequestResult::Success();
}
/**
* Gets the audio mute state of an input.
*
* @requestField inputName | String | Name of input to get the mute state of
*
* @responseField inputMuted | Boolean | Whether the input is muted
*
* @requestType GetInputMute
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::GetInputMute(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -183,11 +386,27 @@ RequestResult RequestHandler::GetInputMute(const Request& request)
if (!input)
return RequestResult::Error(statusCode, comment);
if (!(obs_source_get_output_flags(input) & OBS_SOURCE_AUDIO))
return RequestResult::Error(RequestStatus::InvalidResourceState, "The specified input does not support audio.");
json responseData;
responseData["inputMuted"] = obs_source_muted(input);
return RequestResult::Success(responseData);
}
/**
* Sets the audio mute state of an input.
*
* @requestField inputName | String | Name of the input to set the mute state of
* @requestField inputMuted | Boolean | Whether to mute the input or not
*
* @requestType SetInputMute
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::SetInputMute(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -196,11 +415,28 @@ RequestResult RequestHandler::SetInputMute(const Request& request)
if (!(input && request.ValidateBoolean("inputMuted", statusCode, comment)))
return RequestResult::Error(statusCode, comment);
if (!(obs_source_get_output_flags(input) & OBS_SOURCE_AUDIO))
return RequestResult::Error(RequestStatus::InvalidResourceState, "The specified input does not support audio.");
obs_source_set_muted(input, request.RequestData["inputMuted"]);
return RequestResult::Success();
}
/**
* Toggles the audio mute state of an input.
*
* @requestField inputName | String | Name of the input to toggle the mute state of
*
* @responseField inputMuted | Boolean | Whether the input has been muted or unmuted
*
* @requestType ToggleInputMute
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::ToggleInputMute(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -209,6 +445,9 @@ RequestResult RequestHandler::ToggleInputMute(const Request& request)
if (!input)
return RequestResult::Error(statusCode, comment);
if (!(obs_source_get_output_flags(input) & OBS_SOURCE_AUDIO))
return RequestResult::Error(RequestStatus::InvalidResourceState, "The specified input does not support audio.");
bool inputMuted = !obs_source_muted(input);
obs_source_set_muted(input, inputMuted);
@ -217,6 +456,21 @@ RequestResult RequestHandler::ToggleInputMute(const Request& request)
return RequestResult::Success(responseData);
}
/**
* Gets the current volume setting of an input.
*
* @requestField inputName | String | Name of the input to get the volume of
*
* @responseField inputVolumeMul | Number | Volume setting in mul
* @responseField inputVolumeDb | Number | Volume setting in dB
*
* @requestType GetInputVolume
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::GetInputVolume(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -225,6 +479,9 @@ RequestResult RequestHandler::GetInputVolume(const Request& request)
if (!input)
return RequestResult::Error(statusCode, comment);
if (!(obs_source_get_output_flags(input) & OBS_SOURCE_AUDIO))
return RequestResult::Error(RequestStatus::InvalidResourceState, "The specified input does not support audio.");
float inputVolumeMul = obs_source_get_volume(input);
float inputVolumeDb = obs_mul_to_db(inputVolumeMul);
if (inputVolumeDb == -INFINITY)
@ -236,6 +493,20 @@ RequestResult RequestHandler::GetInputVolume(const Request& request)
return RequestResult::Success(responseData);
}
/**
* Sets the volume setting of an input.
*
* @requestField inputName | String | Name of the input to set the volume of
* @requestField ?inputVolumeMul | Number | Volume setting in mul | >= 0, <= 20 | `inputVolumeDb` should be specified
* @requestField ?inputVolumeDb | Number | Volume setting in dB | >= -100, <= 26 | `inputVolumeMul` should be specified
*
* @requestType SetInputVolume
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::SetInputVolume(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -244,21 +515,24 @@ RequestResult RequestHandler::SetInputVolume(const Request& request)
if (!input)
return RequestResult::Error(statusCode, comment);
bool hasMul = request.ValidateNumber("inputVolumeMul", statusCode, comment, 0, 20);
if (!hasMul && statusCode != RequestStatus::MissingRequestParameter)
if (!(obs_source_get_output_flags(input) & OBS_SOURCE_AUDIO))
return RequestResult::Error(RequestStatus::InvalidResourceState, "The specified input does not support audio.");
bool hasMul = request.Contains("inputVolumeMul");
if (hasMul && !request.ValidateOptionalNumber("inputVolumeMul", statusCode, comment, 0, 20))
return RequestResult::Error(statusCode, comment);
bool hasDb = request.ValidateNumber("inputVolumeDb", statusCode, comment, -100, 26);
if (!hasDb && statusCode != RequestStatus::MissingRequestParameter)
bool hasDb = request.Contains("inputVolumeDb");
if (hasDb && !request.ValidateOptionalNumber("inputVolumeDb", statusCode, comment, -100, 26))
return RequestResult::Error(statusCode, comment);
if (hasMul && hasDb)
return RequestResult::Error(RequestStatus::TooManyRequestParameters, "You may only specify one volume parameter.");
return RequestResult::Error(RequestStatus::TooManyRequestFields, "You may only specify one volume field.");
if (!hasMul && !hasDb)
return RequestResult::Error(RequestStatus::MissingRequestParameter, "You must specify one volume parameter.");
return RequestResult::Error(RequestStatus::MissingRequestField, "You must specify one volume field.");
float inputVolumeMul = 0.0;
float inputVolumeMul;
if (hasMul)
inputVolumeMul = request.RequestData["inputVolumeMul"];
else
@ -268,3 +542,375 @@ RequestResult RequestHandler::SetInputVolume(const Request& request)
return RequestResult::Success();
}
/**
* Gets the audio balance of an input.
*
* @requestField inputName | String | Name of the input to get the audio balance of
*
* @responseField inputAudioBalance | Number | Audio balance value from 0.0-1.0
*
* @requestType GetInputAudioBalance
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::GetInputAudioBalance(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSourceAutoRelease input = request.ValidateInput("inputName", statusCode, comment);
if (!input)
return RequestResult::Error(statusCode, comment);
if (!(obs_source_get_output_flags(input) & OBS_SOURCE_AUDIO))
return RequestResult::Error(RequestStatus::InvalidResourceState, "The specified input does not support audio.");
json responseData;
responseData["inputAudioBalance"] = obs_source_get_balance_value(input);
return RequestResult::Success(responseData);
}
/**
* Sets the audio balance of an input.
*
* @requestField inputName | String | Name of the input to set the audio balance of
* @requestField inputAudioBalance | Number | New audio balance value | >= 0.0, <= 1.0
*
* @requestType SetInputAudioBalance
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::SetInputAudioBalance(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSourceAutoRelease input = request.ValidateInput("inputName", statusCode, comment);
if (!(input && request.ValidateNumber("inputAudioBalance", statusCode, comment, 0.0, 1.0)))
return RequestResult::Error(statusCode, comment);
if (!(obs_source_get_output_flags(input) & OBS_SOURCE_AUDIO))
return RequestResult::Error(RequestStatus::InvalidResourceState, "The specified input does not support audio.");
float inputAudioBalance = request.RequestData["inputAudioBalance"];
obs_source_set_balance_value(input, inputAudioBalance);
return RequestResult::Success();
}
/**
* Gets the audio sync offset of an input.
*
* Note: The audio sync offset can be negative too!
*
* @requestField inputName | String | Name of the input to get the audio sync offset of
*
* @responseField inputAudioSyncOffset | Number | Audio sync offset in milliseconds
*
* @requestType GetInputAudioSyncOffset
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::GetInputAudioSyncOffset(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSourceAutoRelease input = request.ValidateInput("inputName", statusCode, comment);
if (!input)
return RequestResult::Error(statusCode, comment);
if (!(obs_source_get_output_flags(input) & OBS_SOURCE_AUDIO))
return RequestResult::Error(RequestStatus::InvalidResourceState, "The specified input does not support audio.");
json responseData;
// Offset is stored in nanoseconds in OBS.
responseData["inputAudioSyncOffset"] = obs_source_get_sync_offset(input) / 1000000;
return RequestResult::Success(responseData);
}
/**
* Sets the audio sync offset of an input.
*
* @requestField inputName | String | Name of the input to set the audio sync offset of
* @requestField inputAudioSyncOffset | Number | New audio sync offset in milliseconds | >= -950, <= 20000
*
* @requestType SetInputAudioSyncOffset
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::SetInputAudioSyncOffset(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSourceAutoRelease input = request.ValidateInput("inputName", statusCode, comment);
if (!(input && request.ValidateNumber("inputAudioSyncOffset", statusCode, comment, -950, 20000)))
return RequestResult::Error(statusCode, comment);
if (!(obs_source_get_output_flags(input) & OBS_SOURCE_AUDIO))
return RequestResult::Error(RequestStatus::InvalidResourceState, "The specified input does not support audio.");
int64_t syncOffset = request.RequestData["inputAudioSyncOffset"];
obs_source_set_sync_offset(input, syncOffset * 1000000);
return RequestResult::Success();
}
/**
* Gets the audio monitor type of an input.
*
* The available audio monitor types are:
* - `OBS_MONITORING_TYPE_NONE`
* - `OBS_MONITORING_TYPE_MONITOR_ONLY`
* - `OBS_MONITORING_TYPE_MONITOR_AND_OUTPUT`
*
* @requestField inputName | String | Name of the input to get the audio monitor type of
*
* @responseField monitorType | String | Audio monitor type
*
* @requestType GetInputAudioMonitorType
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::GetInputAudioMonitorType(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSourceAutoRelease input = request.ValidateInput("inputName", statusCode, comment);
if (!input)
return RequestResult::Error(statusCode, comment);
if (!(obs_source_get_output_flags(input) & OBS_SOURCE_AUDIO))
return RequestResult::Error(RequestStatus::InvalidResourceState, "The specified input does not support audio.");
json responseData;
responseData["monitorType"] = Utils::Obs::StringHelper::GetInputMonitorType(input);
return RequestResult::Success(responseData);
}
/**
* Sets the audio monitor type of an input.
*
* @requestField inputName | String | Name of the input to set the audio monitor type of
* @requestField monitorType | String | Audio monitor type
*
* @requestType SetInputAudioMonitorType
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::SetInputAudioMonitorType(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSourceAutoRelease input = request.ValidateInput("inputName", statusCode, comment);
if (!(input && request.ValidateString("monitorType", statusCode, comment)))
return RequestResult::Error(statusCode, comment);
if (!(obs_source_get_output_flags(input) & OBS_SOURCE_AUDIO))
return RequestResult::Error(RequestStatus::InvalidResourceState, "The specified input does not support audio.");
if (!obs_audio_monitoring_available())
return RequestResult::Error(RequestStatus::InvalidResourceState, "Audio monitoring is not available on this platform.");
enum obs_monitoring_type monitorType;
std::string monitorTypeString = request.RequestData["monitorType"];
if (monitorTypeString == "OBS_MONITORING_TYPE_NONE")
monitorType = OBS_MONITORING_TYPE_NONE;
else if (monitorTypeString == "OBS_MONITORING_TYPE_MONITOR_ONLY")
monitorType = OBS_MONITORING_TYPE_MONITOR_ONLY;
else if (monitorTypeString == "OBS_MONITORING_TYPE_MONITOR_AND_OUTPUT")
monitorType = OBS_MONITORING_TYPE_MONITOR_AND_OUTPUT;
else
return RequestResult::Error(RequestStatus::InvalidRequestField, std::string("Unknown monitor type: ") + monitorTypeString);
obs_source_set_monitoring_type(input, monitorType);
return RequestResult::Success();
}
/**
* Gets the enable state of all audio tracks of an input.
*
* @requestField inputName | String | Name of the input
*
* @responseField inputAudioTracks | Object | Object of audio tracks and associated enable states
*
* @requestType GetInputAudioTracks
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::GetInputAudioTracks(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSourceAutoRelease input = request.ValidateInput("inputName", statusCode, comment);
if (!input)
return RequestResult::Error(statusCode, comment);
if (!(obs_source_get_output_flags(input) & OBS_SOURCE_AUDIO))
return RequestResult::Error(RequestStatus::InvalidResourceState, "The specified input does not support audio.");
long long tracks = obs_source_get_audio_mixers(input);
json inputAudioTracks;
for (long long i = 0; i < MAX_AUDIO_MIXES; i++) {
inputAudioTracks[std::to_string(i + 1)] = (bool)((tracks >> i) & 1);
}
json responseData;
responseData["inputAudioTracks"] = inputAudioTracks;
return RequestResult::Success(responseData);
}
/**
* Sets the enable state of audio tracks of an input.
*
* @requestField inputName | String | Name of the input
* @requestField inputAudioTracks | Object | Track settings to apply
*
* @requestType SetInputAudioTracks
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::SetInputAudioTracks(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSourceAutoRelease input = request.ValidateInput("inputName", statusCode, comment);
if (!input || !request.ValidateObject("inputAudioTracks", statusCode, comment))
return RequestResult::Error(statusCode, comment);
if (!(obs_source_get_output_flags(input) & OBS_SOURCE_AUDIO))
return RequestResult::Error(RequestStatus::InvalidResourceState, "The specified input does not support audio.");
json inputAudioTracks = request.RequestData["inputAudioTracks"];
long long mixers = obs_source_get_audio_mixers(input);
for (long long i = 0; i < MAX_AUDIO_MIXES; i++) {
std::string track = std::to_string(i + 1);
if (!Utils::Json::Contains(inputAudioTracks, track))
continue;
if (!inputAudioTracks[track].is_boolean())
return RequestResult::Error(RequestStatus::InvalidRequestFieldType, "The value of one of your tracks is not a boolean.");
bool enabled = inputAudioTracks[track];
if (enabled)
mixers |= (1 << i);
else
mixers &= ~(1 << i);
}
// Decided that checking if tracks have actually changed is unnecessary
obs_source_set_audio_mixers(input, mixers);
return RequestResult::Success();
}
/**
* Gets the items of a list property from an input's properties.
*
* Note: Use this in cases where an input provides a dynamic, selectable list of items. For example, display capture, where it provides a list of available displays.
*
* @requestField inputName | String | Name of the input
* @requestField propertyName | String | Name of the list property to get the items of
*
* @responseField propertyItems | Array<Object> | Array of items in the list property
*
* @requestType GetInputPropertiesListPropertyItems
* @complexity 4
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::GetInputPropertiesListPropertyItems(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSourceAutoRelease input = request.ValidateInput("inputName", statusCode, comment);
if (!(input && request.ValidateString("propertyName", statusCode, comment)))
return RequestResult::Error(statusCode, comment);
std::string propertyName = request.RequestData["propertyName"];
OBSPropertiesAutoDestroy inputProperties = obs_source_properties(input);
obs_property_t *property = obs_properties_get(inputProperties, propertyName.c_str());
if (!property)
return RequestResult::Error(RequestStatus::ResourceNotFound, "Unable to find a property by that name.");
if (obs_property_get_type(property) != OBS_PROPERTY_LIST)
return RequestResult::Error(RequestStatus::InvalidResourceType, "The property found is not a list.");
json responseData;
responseData["propertyItems"] = Utils::Obs::ArrayHelper::GetListPropertyItems(property);
return RequestResult::Success(responseData);
}
/**
* Presses a button in the properties of an input.
*
* Note: Use this in cases where there is a button in the properties of an input that cannot be accessed in any other way. For example, browser sources, where there is a refresh button.
*
* @requestField inputName | String | Name of the input
* @requestField propertyName | String | Name of the button property to press
*
* @requestType PressInputPropertiesButton
* @complexity 4
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category inputs
*/
RequestResult RequestHandler::PressInputPropertiesButton(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSourceAutoRelease input = request.ValidateInput("inputName", statusCode, comment);
if (!(input && request.ValidateString("propertyName", statusCode, comment)))
return RequestResult::Error(statusCode, comment);
std::string propertyName = request.RequestData["propertyName"];
OBSPropertiesAutoDestroy inputProperties = obs_source_properties(input);
obs_property_t *property = obs_properties_get(inputProperties, propertyName.c_str());
if (!property)
return RequestResult::Error(RequestStatus::ResourceNotFound, "Unable to find a property by that name.");
if (obs_property_get_type(property) != OBS_PROPERTY_BUTTON)
return RequestResult::Error(RequestStatus::InvalidResourceType, "The property found is not a button.");
if (!obs_property_enabled(property))
return RequestResult::Error(RequestStatus::InvalidResourceState, "The property item found is not enabled.");
obs_property_button_clicked(property, input);
return RequestResult::Success();
}

View File

@ -0,0 +1,198 @@
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#include "RequestHandler.h"
bool IsMediaTimeValid(obs_source_t *input)
{
auto mediaState = obs_source_media_get_state(input);
return mediaState == OBS_MEDIA_STATE_PLAYING || mediaState == OBS_MEDIA_STATE_PAUSED;
}
/**
* Gets the status of a media input.
*
* Media States:
* - `OBS_MEDIA_STATE_NONE`
* - `OBS_MEDIA_STATE_PLAYING`
* - `OBS_MEDIA_STATE_OPENING`
* - `OBS_MEDIA_STATE_BUFFERING`
* - `OBS_MEDIA_STATE_PAUSED`
* - `OBS_MEDIA_STATE_STOPPED`
* - `OBS_MEDIA_STATE_ENDED`
* - `OBS_MEDIA_STATE_ERROR`
*
* @requestField inputName | String | Name of the media input
*
* @responseField mediaState | String | State of the media input
* @responseField mediaDuration | Number | Total duration of the playing media in milliseconds. `null` if not playing
* @responseField mediaCursor | Number | Position of the cursor in milliseconds. `null` if not playing
*
* @requestType GetMediaInputStatus
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category media inputs
*/
RequestResult RequestHandler::GetMediaInputStatus(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSourceAutoRelease input = request.ValidateInput("inputName", statusCode, comment);
if (!input)
return RequestResult::Error(statusCode, comment);
json responseData;
responseData["mediaState"] = Utils::Obs::StringHelper::GetMediaInputState(input);
if (IsMediaTimeValid(input)) {
responseData["mediaDuration"] = obs_source_media_get_duration(input);
responseData["mediaCursor"] = obs_source_media_get_time(input);
} else {
responseData["mediaDuration"] = nullptr;
responseData["mediaCursor"] = nullptr;
}
return RequestResult::Success(responseData);
}
/**
* Sets the cursor position of a media input.
*
* This request does not perform bounds checking of the cursor position.
*
* @requestField inputName | String | Name of the media input
* @requestField mediaCursor | Number | New cursor position to set | >= 0
*
* @requestType SetMediaInputCursor
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category media inputs
*/
RequestResult RequestHandler::SetMediaInputCursor(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSourceAutoRelease input = request.ValidateInput("inputName", statusCode, comment);
if (!(input && request.ValidateNumber("mediaCursor", statusCode, comment, 0)))
return RequestResult::Error(statusCode, comment);
if (!IsMediaTimeValid(input))
return RequestResult::Error(RequestStatus::InvalidResourceState, "The media input must be playing or paused in order to set the cursor position.");
int64_t mediaCursor = request.RequestData["mediaCursor"];
// Yes, we're setting the time without checking if it's valid. Can't baby everything.
obs_source_media_set_time(input, mediaCursor);
return RequestResult::Success();
}
/**
* Offsets the current cursor position of a media input by the specified value.
*
* This request does not perform bounds checking of the cursor position.
*
* @requestField inputName | String | Name of the media input
* @requestField mediaCursorOffset | Number | Value to offset the current cursor position by | None
*
* @requestType OffsetMediaInputCursor
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category media inputs
*/
RequestResult RequestHandler::OffsetMediaInputCursor(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSourceAutoRelease input = request.ValidateInput("inputName", statusCode, comment);
if (!(input && request.ValidateNumber("mediaCursorOffset", statusCode, comment)))
return RequestResult::Error(statusCode, comment);
if (!IsMediaTimeValid(input))
return RequestResult::Error(RequestStatus::InvalidResourceState, "The media input must be playing or paused in order to set the cursor position.");
int64_t mediaCursorOffset = request.RequestData["mediaCursorOffset"];
int64_t mediaCursor = obs_source_media_get_time(input) + mediaCursorOffset;
if (mediaCursor < 0)
mediaCursor = 0;
obs_source_media_set_time(input, mediaCursor);
return RequestResult::Success();
}
/**
* Triggers an action on a media input.
*
* @requestField inputName | String | Name of the media input
* @requestField mediaAction | String | Identifier of the `ObsMediaInputAction` enum
*
* @requestType TriggerMediaInputAction
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category media inputs
*/
RequestResult RequestHandler::TriggerMediaInputAction(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSourceAutoRelease input = request.ValidateInput("inputName", statusCode, comment);
if (!(input && request.ValidateString("mediaAction", statusCode, comment)))
return RequestResult::Error(statusCode, comment);
std::string mediaActionString = request.RequestData["mediaAction"];
auto mediaAction = Utils::Obs::EnumHelper::GetMediaInputAction(mediaActionString);
switch (mediaAction) {
default:
case OBS_WEBSOCKET_MEDIA_INPUT_ACTION_NONE:
return RequestResult::Error(RequestStatus::InvalidRequestField, "You have specified an invalid media input action.");
case OBS_WEBSOCKET_MEDIA_INPUT_ACTION_PLAY:
// Shoutout to whoever implemented this API call like this
obs_source_media_play_pause(input, false);
break;
case OBS_WEBSOCKET_MEDIA_INPUT_ACTION_PAUSE:
obs_source_media_play_pause(input, true);
break;
case OBS_WEBSOCKET_MEDIA_INPUT_ACTION_STOP:
obs_source_media_stop(input);
break;
case OBS_WEBSOCKET_MEDIA_INPUT_ACTION_RESTART:
// I'm only implementing this because I'm nice. I think its a really dumb action.
obs_source_media_restart(input);
break;
case OBS_WEBSOCKET_MEDIA_INPUT_ACTION_NEXT:
obs_source_media_next(input);
break;
case OBS_WEBSOCKET_MEDIA_INPUT_ACTION_PREVIOUS:
obs_source_media_previous(input);
break;
}
return RequestResult::Success();
}

View File

@ -0,0 +1,277 @@
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#include "RequestHandler.h"
static bool VirtualCamAvailable()
{
OBSDataAutoRelease privateData = obs_get_private_data();
if (!privateData)
return false;
return obs_data_get_bool(privateData, "vcamEnabled");
}
static bool ReplayBufferAvailable()
{
OBSOutputAutoRelease output = obs_frontend_get_replay_buffer_output();
return output != nullptr;
}
/**
* Gets the status of the virtualcam output.
*
* @responseField outputActive | Boolean | Whether the output is active
*
* @requestType GetVirtualCamStatus
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category outputs
* @api requests
*/
RequestResult RequestHandler::GetVirtualCamStatus(const Request&)
{
if (!VirtualCamAvailable())
return RequestResult::Error(RequestStatus::InvalidResourceState, "VirtualCam is not available.");
json responseData;
responseData["outputActive"] = obs_frontend_virtualcam_active();
return RequestResult::Success(responseData);
}
/**
* Toggles the state of the virtualcam output.
*
* @responseField outputActive | Boolean | Whether the output is active
*
* @requestType ToggleVirtualCam
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category outputs
* @api requests
*/
RequestResult RequestHandler::ToggleVirtualCam(const Request&)
{
if (!VirtualCamAvailable())
return RequestResult::Error(RequestStatus::InvalidResourceState, "VirtualCam is not available.");
bool outputActive = obs_frontend_virtualcam_active();
if (outputActive)
obs_frontend_stop_virtualcam();
else
obs_frontend_start_virtualcam();
json responseData;
responseData["outputActive"] = !outputActive;
return RequestResult::Success(responseData);
}
/**
* Starts the virtualcam output.
*
* @requestType StartVirtualCam
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category outputs
*/
RequestResult RequestHandler::StartVirtualCam(const Request&)
{
if (!VirtualCamAvailable())
return RequestResult::Error(RequestStatus::InvalidResourceState, "VirtualCam is not available.");
if (obs_frontend_virtualcam_active())
return RequestResult::Error(RequestStatus::OutputRunning);
obs_frontend_start_virtualcam();
return RequestResult::Success();
}
/**
* Stops the virtualcam output.
*
* @requestType StopVirtualCam
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category outputs
*/
RequestResult RequestHandler::StopVirtualCam(const Request&)
{
if (!VirtualCamAvailable())
return RequestResult::Error(RequestStatus::InvalidResourceState, "VirtualCam is not available.");
if (!obs_frontend_virtualcam_active())
return RequestResult::Error(RequestStatus::OutputNotRunning);
obs_frontend_stop_virtualcam();
return RequestResult::Success();
}
/**
* Gets the status of the replay buffer output.
*
* @responseField outputActive | Boolean | Whether the output is active
*
* @requestType GetReplayBufferStatus
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category outputs
* @api requests
*/
RequestResult RequestHandler::GetReplayBufferStatus(const Request&)
{
if (!ReplayBufferAvailable())
return RequestResult::Error(RequestStatus::InvalidResourceState, "Replay buffer is not available.");
json responseData;
responseData["outputActive"] = obs_frontend_replay_buffer_active();
return RequestResult::Success(responseData);
}
/**
* Toggles the state of the replay buffer output.
*
* @responseField outputActive | Boolean | Whether the output is active
*
* @requestType ToggleReplayBuffer
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category outputs
* @api requests
*/
RequestResult RequestHandler::ToggleReplayBuffer(const Request&)
{
if (!ReplayBufferAvailable())
return RequestResult::Error(RequestStatus::InvalidResourceState, "Replay buffer is not available.");
bool outputActive = obs_frontend_replay_buffer_active();
if (outputActive)
obs_frontend_replay_buffer_stop();
else
obs_frontend_replay_buffer_start();
json responseData;
responseData["outputActive"] = !outputActive;
return RequestResult::Success(responseData);
}
/**
* Starts the replay buffer output.
*
* @requestType StartReplayBuffer
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category outputs
*/
RequestResult RequestHandler::StartReplayBuffer(const Request&)
{
if (!ReplayBufferAvailable())
return RequestResult::Error(RequestStatus::InvalidResourceState, "Replay buffer is not available.");
if (obs_frontend_replay_buffer_active())
return RequestResult::Error(RequestStatus::OutputRunning);
obs_frontend_replay_buffer_start();
return RequestResult::Success();
}
/**
* Stops the replay buffer output.
*
* @requestType StopReplayBuffer
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category outputs
*/
RequestResult RequestHandler::StopReplayBuffer(const Request&)
{
if (!ReplayBufferAvailable())
return RequestResult::Error(RequestStatus::InvalidResourceState, "Replay buffer is not available.");
if (!obs_frontend_replay_buffer_active())
return RequestResult::Error(RequestStatus::OutputNotRunning);
obs_frontend_replay_buffer_stop();
return RequestResult::Success();
}
/**
* Saves the contents of the replay buffer output.
*
* @requestType SaveReplayBuffer
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category outputs
*/
RequestResult RequestHandler::SaveReplayBuffer(const Request&)
{
if (!ReplayBufferAvailable())
return RequestResult::Error(RequestStatus::InvalidResourceState, "Replay buffer is not available.");
if (!obs_frontend_replay_buffer_active())
return RequestResult::Error(RequestStatus::OutputNotRunning);
obs_frontend_replay_buffer_save();
return RequestResult::Success();
}
/**
* Gets the filename of the last replay buffer save file.
*
* @responseField savedReplayPath | String | File path
*
* @requestType GetLastReplayBufferReplay
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category outputs
*/
RequestResult RequestHandler::GetLastReplayBufferReplay(const Request&)
{
if (!ReplayBufferAvailable())
return RequestResult::Error(RequestStatus::InvalidResourceState, "Replay buffer is not available.");
if (!obs_frontend_replay_buffer_active())
return RequestResult::Error(RequestStatus::OutputNotRunning);
json responseData;
responseData["savedReplayPath"] = Utils::Obs::StringHelper::GetLastReplayBufferFilePath();
return RequestResult::Success(responseData);
}

View File

@ -0,0 +1,184 @@
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#include "RequestHandler.h"
/**
* Gets the status of the record output.
*
* @responseField outputActive | Boolean | Whether the output is active
* @responseField ouputPaused | Boolean | Whether the output is paused
* @responseField outputTimecode | String | Current formatted timecode string for the output
* @responseField outputDuration | Number | Current duration in milliseconds for the output
* @responseField outputBytes | Number | Number of bytes sent by the output
*
* @requestType GetRecordStatus
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category record
*/
RequestResult RequestHandler::GetRecordStatus(const Request&)
{
OBSOutputAutoRelease recordOutput = obs_frontend_get_recording_output();
uint64_t outputDuration = Utils::Obs::NumberHelper::GetOutputDuration(recordOutput);
json responseData;
responseData["outputActive"] = obs_output_active(recordOutput);
responseData["outputPaused"] = obs_output_paused(recordOutput);
responseData["outputTimecode"] = Utils::Obs::StringHelper::DurationToTimecode(outputDuration);
responseData["outputDuration"] = outputDuration;
responseData["outputBytes"] = (uint64_t)obs_output_get_total_bytes(recordOutput);
return RequestResult::Success(responseData);
}
/**
* Toggles the status of the record output.
*
* @requestType ToggleRecord
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category record
*/
RequestResult RequestHandler::ToggleRecord(const Request&)
{
json responseData;
if (obs_frontend_recording_active()) {
obs_frontend_recording_stop();
responseData["outputActive"] = false;
} else {
obs_frontend_recording_start();
responseData["outputActive"] = true;
}
return RequestResult::Success(responseData);
}
/**
* Starts the record output.
*
* @requestType StartRecord
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category record
*/
RequestResult RequestHandler::StartRecord(const Request&)
{
if (obs_frontend_recording_active())
return RequestResult::Error(RequestStatus::OutputRunning);
// TODO: Call signal directly to perform blocking wait
obs_frontend_recording_start();
return RequestResult::Success();
}
/**
* Stops the record output.
*
* @requestType StopRecord
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category record
*/
RequestResult RequestHandler::StopRecord(const Request&)
{
if (!obs_frontend_recording_active())
return RequestResult::Error(RequestStatus::OutputNotRunning);
// TODO: Call signal directly to perform blocking wait
obs_frontend_recording_stop();
return RequestResult::Success();
}
/**
* Toggles pause on the record output.
*
* @requestType ToggleRecordPause
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category record
*/
RequestResult RequestHandler::ToggleRecordPause(const Request&)
{
json responseData;
if (obs_frontend_recording_paused()) {
obs_frontend_recording_pause(false);
responseData["outputPaused"] = false;
} else {
obs_frontend_recording_pause(true);
responseData["outputPaused"] = true;
}
return RequestResult::Success(responseData);
}
/**
* Pauses the record output.
*
* @requestType PauseRecord
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category record
*/
RequestResult RequestHandler::PauseRecord(const Request&)
{
if (obs_frontend_recording_paused())
return RequestResult::Error(RequestStatus::OutputPaused);
// TODO: Call signal directly to perform blocking wait
obs_frontend_recording_pause(true);
return RequestResult::Success();
}
/**
* Resumes the record output.
*
* @requestType ResumeRecord
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category record
*/
RequestResult RequestHandler::ResumeRecord(const Request&)
{
if (!obs_frontend_recording_paused())
return RequestResult::Error(RequestStatus::OutputNotPaused);
// TODO: Call signal directly to perform blocking wait
obs_frontend_recording_pause(false);
return RequestResult::Success();
}

View File

@ -0,0 +1,712 @@
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#include "RequestHandler.h"
/**
* Gets a list of all scene items in a scene.
*
* Scenes only
*
* @requestField sceneName | String | Name of the scene to get the items of
*
* @responseField sceneItems | Array<Object> | Array of scene items in the scene
*
* @requestType GetSceneItemList
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category scene items
*/
RequestResult RequestHandler::GetSceneItemList(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSourceAutoRelease scene = request.ValidateScene("sceneName", statusCode, comment);
if (!scene)
return RequestResult::Error(statusCode, comment);
json responseData;
responseData["sceneItems"] = Utils::Obs::ArrayHelper::GetSceneItemList(obs_scene_from_source(scene));
return RequestResult::Success(responseData);
}
/**
* Basically GetSceneItemList, but for groups.
*
* Using groups at all in OBS is discouraged, as they are very broken under the hood.
*
* Groups only
*
* @requestField sceneName | String | Name of the group to get the items of
*
* @responseField sceneItems | Array<Object> | Array of scene items in the group
*
* @requestType GetGroupItemList
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category scene items
*/
RequestResult RequestHandler::GetGroupSceneItemList(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSourceAutoRelease scene = request.ValidateScene("sceneName", statusCode, comment, OBS_WEBSOCKET_SCENE_FILTER_GROUP_ONLY);
if (!scene)
return RequestResult::Error(statusCode, comment);
json responseData;
responseData["sceneItems"] = Utils::Obs::ArrayHelper::GetSceneItemList(obs_group_from_source(scene));
return RequestResult::Success(responseData);
}
/**
* Searches a scene for a source, and returns its id.
*
* Scenes and Groups
*
* @requestField sceneName | String | Name of the scene or group to search in
* @requestField sourceName | String | Name of the source to find
*
* @responseField sceneItemId | Number | Numeric ID of the scene item
*
* @requestType GetSceneItemId
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category scene items
*/
RequestResult RequestHandler::GetSceneItemId(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSceneAutoRelease scene = request.ValidateScene2("sceneName", statusCode, comment, OBS_WEBSOCKET_SCENE_FILTER_SCENE_OR_GROUP);
if (!(scene && request.ValidateString("sourceName", statusCode, comment)))
return RequestResult::Error(statusCode, comment);
std::string sourceName = request.RequestData["sourceName"];
OBSSceneItemAutoRelease item = Utils::Obs::SearchHelper::GetSceneItemByName(scene, sourceName);
if (!item)
return RequestResult::Error(RequestStatus::ResourceNotFound, "No scene items were found in the specified scene by that name.");
json responseData;
responseData["sceneItemId"] = obs_sceneitem_get_id(item);
return RequestResult::Success(responseData);
}
/**
* Creates a new scene item using a source.
*
* Scenes only
*
* @requestField sceneName | String | Name of the scene to create the new item in
* @requestField sourceName | String | Name of the source to add to the scene
* @requestField ?sceneItemEnabled | Boolean | Enable state to apply to the scene item on creation | True
*
* @responseField sceneItemId | Number | Numeric ID of the scene item
*
* @requestType CreateSceneItem
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category scene items
*/
RequestResult RequestHandler::CreateSceneItem(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSourceAutoRelease sceneSource = request.ValidateScene("sceneName", statusCode, comment);
if (!sceneSource)
return RequestResult::Error(statusCode, comment);
OBSScene scene = obs_scene_from_source(sceneSource);
OBSSourceAutoRelease source = request.ValidateSource("sourceName", statusCode, comment);
if (!source)
return RequestResult::Error(statusCode, comment);
if (request.RequestData["sceneName"] == request.RequestData["sourceName"])
return RequestResult::Error(RequestStatus::CannotAct, "You cannot create scene item of a scene within itself.");
bool sceneItemEnabled = true;
if (request.Contains("sceneItemEnabled")) {
if (!request.ValidateOptionalBoolean("sceneItemEnabled", statusCode, comment))
return RequestResult::Error(statusCode, comment);
sceneItemEnabled = request.RequestData["sceneItemEnabled"];
}
OBSSceneItemAutoRelease sceneItem = Utils::Obs::ActionHelper::CreateSceneItem(source, scene, sceneItemEnabled);
if (!sceneItem)
return RequestResult::Error(RequestStatus::ResourceCreationFailed, "Failed to create the scene item.");
json responseData;
responseData["sceneItemId"] = obs_sceneitem_get_id(sceneItem);
return RequestResult::Success(responseData);
}
/**
* Removes a scene item from a scene.
*
* Scenes only
*
* @requestField sceneName | String | Name of the scene the item is in
* @requestField sceneItemId | Number | Numeric ID of the scene item | >= 0
*
* @requestType RemoveSceneItem
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category scene items
*/
RequestResult RequestHandler::RemoveSceneItem(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSceneItemAutoRelease sceneItem = request.ValidateSceneItem("sceneName", "sceneItemId", statusCode, comment);
if (!sceneItem)
return RequestResult::Error(statusCode, comment);
// Makes the UI log `User Removed source '[source]' from scene '(null)'`. This is not a problem, just a side effect.
obs_sceneitem_remove(sceneItem);
return RequestResult::Success();
}
/**
* Duplicates a scene item, copying all transform and crop info.
*
* Scenes only
*
* @requestField sceneName | String | Name of the scene the item is in
* @requestField sceneItemId | Number | Numeric ID of the scene item | >= 0
* @requestField ?destinationSceneName | String | Name of the scene to create the duplicated item in | `sceneName` is assumed
*
* @responseField sceneItemId | Number | Numeric ID of the duplicated scene item
*
* @requestType DuplicateSceneItem
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category scene items
*/
RequestResult RequestHandler::DuplicateSceneItem(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSceneItemAutoRelease sceneItem = request.ValidateSceneItem("sceneName", "sceneItemId", statusCode, comment);
if (!sceneItem)
return RequestResult::Error(statusCode, comment);
// Get destination scene
obs_scene_t *destinationScene;
if (request.Contains("destinationSceneName")) {
destinationScene = request.ValidateScene2("destinationSceneName", statusCode, comment);
if (!destinationScene)
return RequestResult::Error(statusCode, comment);
} else {
destinationScene = obs_scene_get_ref(obs_sceneitem_get_scene(sceneItem));
if (!destinationScene)
return RequestResult::Error(RequestStatus::RequestProcessingFailed, "Internal error: Failed to get ref for scene of scene item.");
}
if (obs_sceneitem_is_group(sceneItem) && obs_sceneitem_get_scene(sceneItem) == destinationScene) {
obs_scene_release(destinationScene);
return RequestResult::Error(RequestStatus::ResourceCreationFailed, "Scenes may only have one instance of a group.");
}
// Get scene item details
OBSSource sceneItemSource = obs_sceneitem_get_source(sceneItem);
bool sceneItemEnabled = obs_sceneitem_visible(sceneItem);
obs_transform_info sceneItemTransform;
obs_sceneitem_crop sceneItemCrop;
obs_sceneitem_get_info(sceneItem, &sceneItemTransform);
obs_sceneitem_get_crop(sceneItem, &sceneItemCrop);
// Create the new item
OBSSceneItemAutoRelease newSceneItem = Utils::Obs::ActionHelper::CreateSceneItem(sceneItemSource, destinationScene, sceneItemEnabled, &sceneItemTransform, &sceneItemCrop);
obs_scene_release(destinationScene);
if (!newSceneItem)
return RequestResult::Error(RequestStatus::ResourceCreationFailed, "Failed to create the scene item.");
json responseData;
responseData["sceneItemId"] = obs_sceneitem_get_id(newSceneItem);
return RequestResult::Success(responseData);
}
/**
* Gets the transform and crop info of a scene item.
*
* Scenes and Groups
*
* @requestField sceneName | String | Name of the scene the item is in
* @requestField sceneItemId | Number | Numeric ID of the scene item | >= 0
*
* @responseField sceneItemTransform | Object | Object containing scene item transform info
*
* @requestType GetSceneItemTransform
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category scene items
*/
RequestResult RequestHandler::GetSceneItemTransform(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSceneItemAutoRelease sceneItem = request.ValidateSceneItem("sceneName", "sceneItemId", statusCode, comment, OBS_WEBSOCKET_SCENE_FILTER_SCENE_OR_GROUP);
if (!sceneItem)
return RequestResult::Error(statusCode, comment);
json responseData;
responseData["sceneItemTransform"] = Utils::Obs::ObjectHelper::GetSceneItemTransform(sceneItem);
return RequestResult::Success(responseData);
}
/**
* Sets the transform and crop info of a scene item.
*
* @requestField sceneName | String | Name of the scene the item is in
* @requestField sceneItemId | Number | Numeric ID of the scene item | >= 0
* @requestField sceneItemTransform | Object | Object containing scene item transform info to update
*
* @requestType SetSceneItemTransform
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category scene items
*/
RequestResult RequestHandler::SetSceneItemTransform(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSceneItemAutoRelease sceneItem = request.ValidateSceneItem("sceneName", "sceneItemId", statusCode, comment, OBS_WEBSOCKET_SCENE_FILTER_SCENE_OR_GROUP);
if (!(sceneItem && request.ValidateObject("sceneItemTransform", statusCode, comment)))
return RequestResult::Error(statusCode, comment);
// Create a fake request to use checks on the sub object
Request r("", request.RequestData["sceneItemTransform"]);
bool transformChanged = false;
bool cropChanged = false;
obs_transform_info sceneItemTransform;
obs_sceneitem_crop sceneItemCrop;
obs_sceneitem_get_info(sceneItem, &sceneItemTransform);
obs_sceneitem_get_crop(sceneItem, &sceneItemCrop);
OBSSource source = obs_sceneitem_get_source(sceneItem);
float sourceWidth = float(obs_source_get_width(source));
float sourceHeight = float(obs_source_get_height(source));
if (r.Contains("positionX")) {
if (!r.ValidateOptionalNumber("positionX", statusCode, comment, -90001.0, 90001.0))
return RequestResult::Error(statusCode, comment);
sceneItemTransform.pos.x = r.RequestData["positionX"];
transformChanged = true;
}
if (r.Contains("positionY")) {
if (!r.ValidateOptionalNumber("positionY", statusCode, comment, -90001.0, 90001.0))
return RequestResult::Error(statusCode, comment);
sceneItemTransform.pos.y = r.RequestData["positionY"];
transformChanged = true;
}
if (r.Contains("rotation")) {
if (!r.ValidateOptionalNumber("rotation", statusCode, comment, -360.0, 360.0))
return RequestResult::Error(statusCode, comment);
sceneItemTransform.rot = r.RequestData["rotation"];
transformChanged = true;
}
if (r.Contains("scaleX")) {
if (!r.ValidateOptionalNumber("scaleX", statusCode, comment))
return RequestResult::Error(statusCode, comment);
float scaleX = r.RequestData["scaleX"];
float finalWidth = scaleX * sourceWidth;
if (!(finalWidth > -90001.0 && finalWidth < 90001.0))
return RequestResult::Error(RequestStatus::RequestFieldOutOfRange, "The field scaleX is too small or large for the current source resolution.");
sceneItemTransform.scale.x = scaleX;
transformChanged = true;
}
if (r.Contains("scaleY")) {
if (!r.ValidateOptionalNumber("scaleY", statusCode, comment, -90001.0, 90001.0))
return RequestResult::Error(statusCode, comment);
float scaleY = r.RequestData["scaleY"];
float finalHeight = scaleY * sourceHeight;
if (!(finalHeight > -90001.0 && finalHeight < 90001.0))
return RequestResult::Error(RequestStatus::RequestFieldOutOfRange, "The field scaleY is too small or large for the current source resolution.");
sceneItemTransform.scale.y = scaleY;
transformChanged = true;
}
if (r.Contains("alignment")) {
if (!r.ValidateOptionalNumber("alignment", statusCode, comment, 0, std::numeric_limits<uint32_t>::max()))
return RequestResult::Error(statusCode, comment);
sceneItemTransform.alignment = r.RequestData["alignment"];
transformChanged = true;
}
if (r.Contains("boundsType")) {
if (!r.ValidateOptionalString("boundsType", statusCode, comment))
return RequestResult::Error(statusCode, comment);
std::string boundsTypeString = r.RequestData["boundsType"];
enum obs_bounds_type boundsType = Utils::Obs::EnumHelper::GetSceneItemBoundsType(boundsTypeString);
if (boundsType == OBS_BOUNDS_NONE && boundsTypeString != "OBS_BOUNDS_NONE")
return RequestResult::Error(RequestStatus::InvalidRequestField, "The field boundsType has an invalid value.");
sceneItemTransform.bounds_type = boundsType;
transformChanged = true;
}
if (r.Contains("boundsAlignment")) {
if (!r.ValidateOptionalNumber("boundsAlignment", statusCode, comment, 0, std::numeric_limits<uint32_t>::max()))
return RequestResult::Error(statusCode, comment);
sceneItemTransform.bounds_alignment = r.RequestData["boundsAlignment"];
transformChanged = true;
}
if (r.Contains("boundsWidth")) {
if (!r.ValidateOptionalNumber("boundsWidth", statusCode, comment, 1.0, 90001.0))
return RequestResult::Error(statusCode, comment);
sceneItemTransform.bounds.x = r.RequestData["boundsWidth"];
transformChanged = true;
}
if (r.Contains("boundsHeight")) {
if (!r.ValidateOptionalNumber("boundsHeight", statusCode, comment, 1.0, 90001.0))
return RequestResult::Error(statusCode, comment);
sceneItemTransform.bounds.y = r.RequestData["boundsHeight"];
transformChanged = true;
}
if (r.Contains("cropLeft")) {
if (!r.ValidateOptionalNumber("cropLeft", statusCode, comment, 0.0, 100000.0))
return RequestResult::Error(statusCode, comment);
sceneItemCrop.left = r.RequestData["cropLeft"];
cropChanged = true;
}
if (r.Contains("cropRight")) {
if (!r.ValidateOptionalNumber("cropRight", statusCode, comment, 0.0, 100000.0))
return RequestResult::Error(statusCode, comment);
sceneItemCrop.right = r.RequestData["cropRight"];
cropChanged = true;
}
if (r.Contains("cropTop")) {
if (!r.ValidateOptionalNumber("cropTop", statusCode, comment, 0.0, 100000.0))
return RequestResult::Error(statusCode, comment);
sceneItemCrop.top = r.RequestData["cropTop"];
cropChanged = true;
}
if (r.Contains("cropBottom")) {
if (!r.ValidateOptionalNumber("cropBottom", statusCode, comment, 0.0, 100000.0))
return RequestResult::Error(statusCode, comment);
sceneItemCrop.bottom = r.RequestData["cropBottom"];
cropChanged = true;
}
if (!transformChanged && !cropChanged)
return RequestResult::Error(RequestStatus::CannotAct, "You have not provided any valid transform changes.");
if (transformChanged)
obs_sceneitem_set_info(sceneItem, &sceneItemTransform);
if (cropChanged)
obs_sceneitem_set_crop(sceneItem, &sceneItemCrop);
return RequestResult::Success();
}
/**
* Gets the enable state of a scene item.
*
* Scenes and Groups
*
* @requestField sceneName | String | Name of the scene the item is in
* @requestField sceneItemId | Number | Numeric ID of the scene item | >= 0
*
* @responseField sceneItemEnabled | Boolean | Whether the scene item is enabled. `true` for enabled, `false` for disabled
*
* @requestType GetSceneItemEnabled
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category scene items
*/
RequestResult RequestHandler::GetSceneItemEnabled(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSceneItemAutoRelease sceneItem = request.ValidateSceneItem("sceneName", "sceneItemId", statusCode, comment, OBS_WEBSOCKET_SCENE_FILTER_SCENE_OR_GROUP);
if (!sceneItem)
return RequestResult::Error(statusCode, comment);
json responseData;
responseData["sceneItemEnabled"] = obs_sceneitem_visible(sceneItem);
return RequestResult::Success(responseData);
}
/**
* Sets the enable state of a scene item.
*
* Scenes and Groups
*
* @requestField sceneName | String | Name of the scene the item is in
* @requestField sceneItemId | Number | Numeric ID of the scene item | >= 0
* @requestField sceneItemEnabled | Boolean | New enable state of the scene item
*
* @requestType SetSceneItemEnabled
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category scene items
*/
RequestResult RequestHandler::SetSceneItemEnabled(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSceneItemAutoRelease sceneItem = request.ValidateSceneItem("sceneName", "sceneItemId", statusCode, comment, OBS_WEBSOCKET_SCENE_FILTER_SCENE_OR_GROUP);
if (!(sceneItem && request.ValidateBoolean("sceneItemEnabled", statusCode, comment)))
return RequestResult::Error(statusCode, comment);
bool sceneItemEnabled = request.RequestData["sceneItemEnabled"];
obs_sceneitem_set_visible(sceneItem, sceneItemEnabled);
return RequestResult::Success();
}
/**
* Gets the lock state of a scene item.
*
* Scenes and Groups
*
* @requestField sceneName | String | Name of the scene the item is in
* @requestField sceneItemId | Number | Numeric ID of the scene item | >= 0
*
* @responseField sceneItemLocked | Boolean | Whether the scene item is locked. `true` for locked, `false` for unlocked
*
* @requestType GetSceneItemLocked
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category scene items
*/
RequestResult RequestHandler::GetSceneItemLocked(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSceneItemAutoRelease sceneItem = request.ValidateSceneItem("sceneName", "sceneItemId", statusCode, comment, OBS_WEBSOCKET_SCENE_FILTER_SCENE_OR_GROUP);
if (!sceneItem)
return RequestResult::Error(statusCode, comment);
json responseData;
responseData["sceneItemLocked"] = obs_sceneitem_locked(sceneItem);
return RequestResult::Success(responseData);
}
/**
* Sets the lock state of a scene item.
*
* Scenes and Group
*
* @requestField sceneName | String | Name of the scene the item is in
* @requestField sceneItemId | Number | Numeric ID of the scene item | >= 0
* @requestField sceneItemLocked | Boolean | New lock state of the scene item
*
* @requestType SetSceneItemLocked
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category scene items
*/
RequestResult RequestHandler::SetSceneItemLocked(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSceneItemAutoRelease sceneItem = request.ValidateSceneItem("sceneName", "sceneItemId", statusCode, comment, OBS_WEBSOCKET_SCENE_FILTER_SCENE_OR_GROUP);
if (!(sceneItem && request.ValidateBoolean("sceneItemLocked", statusCode, comment)))
return RequestResult::Error(statusCode, comment);
bool sceneItemLocked = request.RequestData["sceneItemLocked"];
obs_sceneitem_set_locked(sceneItem, sceneItemLocked);
return RequestResult::Success();
}
/**
* Gets the index position of a scene item in a scene.
*
* An index of 0 is at the bottom of the source list in the UI.
*
* Scenes and Groups
*
* @requestField sceneName | String | Name of the scene the item is in
* @requestField sceneItemId | Number | Numeric ID of the scene item | >= 0
*
* @responseField sceneItemIndex | Number | Index position of the scene item
*
* @requestType GetSceneItemIndex
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category scene items
*/
RequestResult RequestHandler::GetSceneItemIndex(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSceneItemAutoRelease sceneItem = request.ValidateSceneItem("sceneName", "sceneItemId", statusCode, comment, OBS_WEBSOCKET_SCENE_FILTER_SCENE_OR_GROUP);
if (!sceneItem)
return RequestResult::Error(statusCode, comment);
json responseData;
responseData["sceneItemIndex"] = obs_sceneitem_get_order_position(sceneItem);
return RequestResult::Success(responseData);
}
/**
* Sets the index position of a scene item in a scene.
*
* Scenes and Groups
*
* @requestField sceneName | String | Name of the scene the item is in
* @requestField sceneItemId | Number | Numeric ID of the scene item | >= 0
* @requestField sceneItemIndex | Number | New index position of the scene item | >= 0
*
* @requestType SetSceneItemIndex
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category scene items
*/
RequestResult RequestHandler::SetSceneItemIndex(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSceneItemAutoRelease sceneItem = request.ValidateSceneItem("sceneName", "sceneItemId", statusCode, comment, OBS_WEBSOCKET_SCENE_FILTER_SCENE_OR_GROUP);
if (!(sceneItem && request.ValidateNumber("sceneItemIndex", statusCode, comment, 0, 8192)))
return RequestResult::Error(statusCode, comment);
int sceneItemIndex = request.RequestData["sceneItemIndex"];
obs_sceneitem_set_order_position(sceneItem, sceneItemIndex);
return RequestResult::Success();
}
/**
* Gets the blend mode of a scene item.
*
* Blend modes:
*
* - `OBS_BLEND_NORMAL`
* - `OBS_BLEND_ADDITIVE`
* - `OBS_BLEND_SUBTRACT`
* - `OBS_BLEND_SCREEN`
* - `OBS_BLEND_MULTIPLY`
* - `OBS_BLEND_LIGHTEN`
* - `OBS_BLEND_DARKEN`
*
* Scenes and Groups
*
* @requestField sceneName | String | Name of the scene the item is in
* @requestField sceneItemId | Number | Numeric ID of the scene item | >= 0
*
* @responseField sceneItemBlendMode | String | Current blend mode
*
* @requestType GetSceneItemBlendMode
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category scene items
*/
RequestResult RequestHandler::GetSceneItemBlendMode(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSceneItemAutoRelease sceneItem = request.ValidateSceneItem("sceneName", "sceneItemId", statusCode, comment, OBS_WEBSOCKET_SCENE_FILTER_SCENE_OR_GROUP);
if (!sceneItem)
return RequestResult::Error(statusCode, comment);
auto blendMode = obs_sceneitem_get_blending_mode(sceneItem);
json responseData;
responseData["sceneItemBlendMode"] = Utils::Obs::StringHelper::GetSceneItemBlendMode(blendMode);
return RequestResult::Success(responseData);
}
/**
* Sets the blend mode of a scene item.
*
* Scenes and Groups
*
* @requestField sceneName | String | Name of the scene the item is in
* @requestField sceneItemId | Number | Numeric ID of the scene item | >= 0
* @requestField sceneItemBlendMode | String | New blend mode
*
* @requestType SetSceneItemBlendMode
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category scene items
*/
RequestResult RequestHandler::SetSceneItemBlendMode(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSceneItemAutoRelease sceneItem = request.ValidateSceneItem("sceneName", "sceneItemId", statusCode, comment, OBS_WEBSOCKET_SCENE_FILTER_SCENE_OR_GROUP);
if (!(sceneItem && request.ValidateString("sceneItemBlendMode", statusCode, comment)))
return RequestResult::Error(statusCode, comment);
std::string blendModeString = request.RequestData["sceneItemBlendMode"];
auto blendMode = Utils::Obs::EnumHelper::GetSceneItemBlendMode(blendModeString);
if (blendMode == OBS_BLEND_NORMAL && blendModeString != "OBS_BLEND_NORMAL")
return RequestResult::Error(RequestStatus::InvalidRequestField, "The field sceneItemBlendMode has an invalid value.");
obs_sceneitem_set_blending_mode(sceneItem, blendMode);
return RequestResult::Success();
}

View File

@ -1,12 +1,47 @@
#include "RequestHandler.h"
#include "../plugin-macros.generated.h"
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
RequestResult RequestHandler::GetSceneList(const Request& request)
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#include "RequestHandler.h"
/**
* Gets an array of all scenes in OBS.
*
* @responseField currentProgramSceneName | String | Current program scene
* @responseField currentPreviewSceneName | String | Current preview scene. `null` if not in studio mode
* @responseField scenes | Array<Object> | Array of scenes
*
* @requestType GetSceneList
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category scenes
*/
RequestResult RequestHandler::GetSceneList(const Request&)
{
json responseData;
OBSSourceAutoRelease currentProgramScene = obs_frontend_get_current_scene();
responseData["currentProgramSceneName"] = obs_source_get_name(currentProgramScene);
if (currentProgramScene)
responseData["currentProgramSceneName"] = obs_source_get_name(currentProgramScene);
else
responseData["currentProgramSceneName"] = nullptr;
OBSSourceAutoRelease currentPreviewScene = obs_frontend_get_current_preview_scene();
if (currentPreviewScene)
@ -14,19 +49,67 @@ RequestResult RequestHandler::GetSceneList(const Request& request)
else
responseData["currentPreviewSceneName"] = nullptr;
responseData["scenes"] = Utils::Obs::ListHelper::GetSceneList();
responseData["scenes"] = Utils::Obs::ArrayHelper::GetSceneList();
return RequestResult::Success(responseData);
}
RequestResult RequestHandler::GetCurrentProgramScene(const Request& request)
/**
* Gets an array of all groups in OBS.
*
* Groups in OBS are actually scenes, but renamed and modified. In obs-websocket, we treat them as scenes where we can.
*
* @responseField groups | Array<String> | Array of group names
*
* @requestType GetGroupList
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category scenes
*/
RequestResult RequestHandler::GetGroupList(const Request&)
{
json responseData;
responseData["groups"] = Utils::Obs::ArrayHelper::GetGroupList();
return RequestResult::Success(responseData);
}
/**
* Gets the current program scene.
*
* @responseField currentProgramSceneName | String | Current program scene
*
* @requestType GetCurrentProgramScene
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category scenes
*/
RequestResult RequestHandler::GetCurrentProgramScene(const Request&)
{
json responseData;
OBSSourceAutoRelease currentProgramScene = obs_frontend_get_current_scene();
responseData["currentProgramSceneName"] = obs_source_get_name(currentProgramScene);
return RequestResult::Success(responseData);
}
/**
* Sets the current program scene.
*
* @requestField sceneName | String | Scene to set as the current program scene
*
* @requestType SetCurrentProgramScene
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category scenes
*/
RequestResult RequestHandler::SetCurrentProgramScene(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -40,7 +123,21 @@ RequestResult RequestHandler::SetCurrentProgramScene(const Request& request)
return RequestResult::Success();
}
RequestResult RequestHandler::GetCurrentPreviewScene(const Request& request)
/**
* Gets the current preview scene.
*
* Only available when studio mode is enabled.
*
* @responseField currentPreviewSceneName | String | Current preview scene
*
* @requestType GetCurrentPreviewScene
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category scenes
*/
RequestResult RequestHandler::GetCurrentPreviewScene(const Request&)
{
if (!obs_frontend_preview_program_mode_active())
return RequestResult::Error(RequestStatus::StudioModeNotActive);
@ -49,9 +146,24 @@ RequestResult RequestHandler::GetCurrentPreviewScene(const Request& request)
json responseData;
responseData["currentPreviewSceneName"] = obs_source_get_name(currentPreviewScene);
return RequestResult::Success(responseData);
}
/**
* Sets the current preview scene.
*
* Only available when studio mode is enabled.
*
* @requestField sceneName | String | Scene to set as the current preview scene
*
* @requestType SetCurrentPreviewScene
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category scenes
*/
RequestResult RequestHandler::SetCurrentPreviewScene(const Request& request)
{
if (!obs_frontend_preview_program_mode_active())
@ -68,6 +180,18 @@ RequestResult RequestHandler::SetCurrentPreviewScene(const Request& request)
return RequestResult::Success();
}
/**
* Creates a new scene in OBS.
*
* @requestField sceneName | String | Name for the new scene
*
* @requestType CreateScene
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category scenes
*/
RequestResult RequestHandler::CreateScene(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -79,14 +203,29 @@ RequestResult RequestHandler::CreateScene(const Request& request)
OBSSourceAutoRelease scene = obs_get_source_by_name(sceneName.c_str());
if (scene)
return RequestResult::Error(RequestStatus::SourceAlreadyExists, "A source already exists by that scene name.");
return RequestResult::Error(RequestStatus::ResourceAlreadyExists, "A source already exists by that scene name.");
obs_scene_t *createdScene = obs_scene_create(sceneName.c_str());
if (!createdScene)
return RequestResult::Error(RequestStatus::ResourceCreationFailed, "Failed to create the scene.");
obs_scene_release(createdScene);
return RequestResult::Success();
}
/**
* Removes a scene from OBS.
*
* @requestField sceneName | String | Name of the scene to remove
*
* @requestType RemoveScene
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category scenes
*/
RequestResult RequestHandler::RemoveScene(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -95,11 +234,27 @@ RequestResult RequestHandler::RemoveScene(const Request& request)
if (!scene)
return RequestResult::Error(statusCode, comment);
if (Utils::Obs::NumberHelper::GetSceneCount() < 2)
return RequestResult::Error(RequestStatus::NotEnoughResources, "You cannot remove the last scene in the collection.");
obs_source_remove(scene);
return RequestResult::Success();
}
/**
* Sets the name of a scene (rename).
*
* @requestField sceneName | String | Name of the scene to be renamed
* @requestField newSceneName | String | New name for the scene
*
* @requestType SetSceneName
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category scenes
*/
RequestResult RequestHandler::SetSceneName(const Request& request)
{
RequestStatus::RequestStatus statusCode;
@ -112,9 +267,111 @@ RequestResult RequestHandler::SetSceneName(const Request& request)
OBSSourceAutoRelease existingSource = obs_get_source_by_name(newSceneName.c_str());
if (existingSource)
return RequestResult::Error(RequestStatus::SourceAlreadyExists, "A source already exists by that new scene name.");
return RequestResult::Error(RequestStatus::ResourceAlreadyExists, "A source already exists by that new scene name.");
obs_source_set_name(scene, newSceneName.c_str());
return RequestResult::Success();
}
/**
* Gets the scene transition overridden for a scene.
*
* @requestField sceneName | String | Name of the scene
*
* @responseField transitionName | String | Name of the overridden scene transition, else `null`
* @responseField transitionDuration | Number | Duration of the overridden scene transition, else `null`
*
* @requestType GetSceneSceneTransitionOverride
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category scenes
*/
RequestResult RequestHandler::GetSceneSceneTransitionOverride(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSourceAutoRelease scene = request.ValidateScene("sceneName", statusCode, comment);
if (!scene)
return RequestResult::Error(statusCode, comment);
OBSDataAutoRelease privateSettings = obs_source_get_private_settings(scene);
json responseData;
const char *transitionName = obs_data_get_string(privateSettings, "transition");
if (transitionName && strlen(transitionName))
responseData["transitionName"] = transitionName;
else
responseData["transitionName"] = nullptr;
if (obs_data_has_user_value(privateSettings, "transition_duration"))
responseData["transitionDuration"] = obs_data_get_int(privateSettings, "transition_duration");
else
responseData["transitionDuration"] = nullptr;
return RequestResult::Success(responseData);
}
/**
* Gets the scene transition overridden for a scene.
*
* @requestField sceneName | String | Name of the scene
* @requestField ?transitionName | String | Name of the scene transition to use as override. Specify `null` to remove | Unchanged
* @requestField ?transitionDuration | Number | Duration to use for any overridden transition. Specify `null` to remove | >= 50, <= 20000 | Unchanged
*
* @requestType SetSceneSceneTransitionOverride
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category scenes
*/
RequestResult RequestHandler::SetSceneSceneTransitionOverride(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSourceAutoRelease scene = request.ValidateScene("sceneName", statusCode, comment);
if (!scene)
return RequestResult::Error(statusCode, comment);
OBSDataAutoRelease privateSettings = obs_source_get_private_settings(scene);
bool hasName = request.RequestData.contains("transitionName");
if (hasName && !request.RequestData["transitionName"].is_null()) {
if (!request.ValidateOptionalString("transitionName", statusCode, comment))
return RequestResult::Error(statusCode, comment);
OBSSourceAutoRelease transition = Utils::Obs::SearchHelper::GetSceneTransitionByName(request.RequestData["transitionName"]);
if (!transition)
return RequestResult::Error(RequestStatus::ResourceNotFound, "No scene transition was found by that name.");
}
bool hasDuration = request.RequestData.contains("transitionDuration");
if (hasDuration && !request.RequestData["transitionDuration"].is_null()) {
if (!request.ValidateOptionalNumber("transitionDuration", statusCode, comment, 50, 20000))
return RequestResult::Error(statusCode, comment);
}
if (!hasName && !hasDuration)
return RequestResult::Error(RequestStatus::MissingRequestField, "Your request data must include either `transitionName` or `transitionDuration`.");
if (hasName) {
if (request.RequestData["transitionName"].is_null()) {
obs_data_erase(privateSettings, "transition");
} else {
std::string transitionName = request.RequestData["transitionName"];
obs_data_set_string(privateSettings, "transition", transitionName.c_str());
}
}
if (hasDuration) {
if (request.RequestData["transitionDuration"].is_null()) {
obs_data_erase(privateSettings, "transition_duration");
} else {
obs_data_set_int(privateSettings, "transition_duration", request.RequestData["transitionDuration"]);
}
}
return RequestResult::Success();
}

View File

@ -1,3 +1,22 @@
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#include <QBuffer>
#include <QImageWriter>
#include <QFileInfo>
@ -5,7 +24,6 @@
#include <QDir>
#include "RequestHandler.h"
#include "../plugin-macros.generated.h"
QImage TakeSourceScreenshot(obs_source_t *source, bool &success, uint32_t requestedWidth = 0, uint32_t requestedHeight = 0)
{
@ -91,21 +109,33 @@ bool IsImageFormatValid(std::string format)
return supportedFormats.contains(format.c_str());
}
/**
* Gets the active and show state of a source.
*
* **Compatible with inputs and scenes.**
*
* @requestField sourceName | String | Name of the source to get the active state of
*
* @responseField videoActive | Boolean | Whether the source is showing in Program
* @responseField videoShowing | Boolean | Whether the source is showing in the UI (Preview, Projector, Properties)
*
* @requestType GetSourceActive
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category sources
*/
RequestResult RequestHandler::GetSourceActive(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
if (!request.ValidateString("sourceName", statusCode, comment))
OBSSourceAutoRelease source = request.ValidateSource("sourceName", statusCode, comment);
if (!source)
return RequestResult::Error(statusCode, comment);
std::string sourceName = request.RequestData["sourceName"];
OBSSourceAutoRelease source = obs_get_source_by_name(sourceName.c_str());
if (!source)
return RequestResult::Error(RequestStatus::SourceNotFound);
if (obs_source_get_type(source) != OBS_SOURCE_TYPE_INPUT && obs_source_get_type(source) != OBS_SOURCE_TYPE_SCENE)
return RequestResult::Error(RequestStatus::InvalidSourceType, "The specified source is not an input or a scene.");
return RequestResult::Error(RequestStatus::InvalidResourceType, "The specified source is not an input or a scene.");
json responseData;
responseData["videoActive"] = obs_source_active(source);
@ -113,47 +143,65 @@ RequestResult RequestHandler::GetSourceActive(const Request& request)
return RequestResult::Success(responseData);
}
/**
* Gets a Base64-encoded screenshot of a source.
*
* The `imageWidth` and `imageHeight` parameters are treated as "scale to inner", meaning the smallest ratio will be used and the aspect ratio of the original resolution is kept.
* If `imageWidth` and `imageHeight` are not specified, the compressed image will use the full resolution of the source.
*
* **Compatible with inputs and scenes.**
*
* @requestField sourceName | String | Name of the source to take a screenshot of
* @requestField imageFormat | String | Image compression format to use. Use `GetVersion` to get compatible image formats
* @requestField ?imageWidth | Number | Width to scale the screenshot to | >= 8, <= 4096 | Source value is used
* @requestField ?imageHeight | Number | Height to scale the screenshot to | >= 8, <= 4096 | Source value is used
* @requestField ?imageCompressionQuality | Number | Compression quality to use. 0 for high compression, 100 for uncompressed. -1 to use "default" (whatever that means, idk) | >= -1, <= 100 | -1
*
* @responseField imageData | String | Base64-encoded screenshot
*
* @requestType GetSourceScreenshot
* @complexity 4
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category sources
*/
RequestResult RequestHandler::GetSourceScreenshot(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
if (!(request.ValidateString("sourceName", statusCode, comment) && request.ValidateString("imageFormat", statusCode, comment)))
OBSSourceAutoRelease source = request.ValidateSource("sourceName", statusCode, comment);
if (!(source && request.ValidateString("imageFormat", statusCode, comment)))
return RequestResult::Error(statusCode, comment);
std::string sourceName = request.RequestData["sourceName"];
OBSSourceAutoRelease source = obs_get_source_by_name(sourceName.c_str());
if (!source)
return RequestResult::Error(RequestStatus::SourceNotFound);
if (obs_source_get_type(source) != OBS_SOURCE_TYPE_INPUT && obs_source_get_type(source) != OBS_SOURCE_TYPE_SCENE)
return RequestResult::Error(RequestStatus::InvalidSourceType, "The specified source is not an input or a scene.");
return RequestResult::Error(RequestStatus::InvalidResourceType, "The specified source is not an input or a scene.");
std::string imageFormat = request.RequestData["imageFormat"];
if (!IsImageFormatValid(imageFormat))
return RequestResult::Error(RequestStatus::InvalidRequestParameter, "Your specified image format is invalid or not supported by this system.");
return RequestResult::Error(RequestStatus::InvalidRequestField, "Your specified image format is invalid or not supported by this system.");
uint32_t requestedWidth{0};
uint32_t requestedHeight{0};
int compressionQuality{-1};
if (request.RequestData.contains("imageWidth") && !request.RequestData["imageWidth"].is_null()) {
if (!request.ValidateNumber("imageWidth", statusCode, comment, 8, 4096))
if (request.Contains("imageWidth")) {
if (!request.ValidateOptionalNumber("imageWidth", statusCode, comment, 8, 4096))
return RequestResult::Error(statusCode, comment);
requestedWidth = request.RequestData["imageWidth"];
}
if (request.RequestData.contains("imageHeight") && !request.RequestData["imageHeight"].is_null()) {
if (!request.ValidateNumber("imageHeight", statusCode, comment, 8, 4096))
if (request.Contains("imageHeight")) {
if (!request.ValidateOptionalNumber("imageHeight", statusCode, comment, 8, 4096))
return RequestResult::Error(statusCode, comment);
requestedHeight = request.RequestData["imageHeight"];
}
if (request.RequestData.contains("imageCompressionQuality") && !request.RequestData["imageCompressionQuality"].is_null()) {
if (!request.ValidateNumber("imageCompressionQuality", statusCode, comment, -1, 100))
if (request.Contains("imageCompressionQuality")) {
if (!request.ValidateOptionalNumber("imageCompressionQuality", statusCode, comment, -1, 100))
return RequestResult::Error(statusCode, comment);
compressionQuality = request.RequestData["imageCompressionQuality"];
@ -163,14 +211,14 @@ RequestResult RequestHandler::GetSourceScreenshot(const Request& request)
QImage renderedImage = TakeSourceScreenshot(source, success, requestedWidth, requestedHeight);
if (!success)
return RequestResult::Error(RequestStatus::ScreenshotRenderFailed);
return RequestResult::Error(RequestStatus::RequestProcessingFailed, "Failed to render screenshot.");
QByteArray encodedImgBytes;
QBuffer buffer(&encodedImgBytes);
buffer.open(QBuffer::WriteOnly);
if (!renderedImage.save(&buffer, imageFormat.c_str(), compressionQuality))
return RequestResult::Error(RequestStatus::ScreenshotEncodeFailed);
return RequestResult::Error(RequestStatus::RequestProcessingFailed, "Failed to encode screenshot.");
buffer.close();
@ -181,47 +229,71 @@ RequestResult RequestHandler::GetSourceScreenshot(const Request& request)
return RequestResult::Success(responseData);
}
/**
* Saves a screenshot of a source to the filesystem.
*
* The `imageWidth` and `imageHeight` parameters are treated as "scale to inner", meaning the smallest ratio will be used and the aspect ratio of the original resolution is kept.
* If `imageWidth` and `imageHeight` are not specified, the compressed image will use the full resolution of the source.
*
* **Compatible with inputs and scenes.**
*
* @requestField sourceName | String | Name of the source to take a screenshot of
* @requestField imageFormat | String | Image compression format to use. Use `GetVersion` to get compatible image formats
* @requestField imageFilePath | String | Path to save the screenshot file to. Eg. `C:\Users\user\Desktop\screenshot.png`
* @requestField ?imageWidth | Number | Width to scale the screenshot to | >= 8, <= 4096 | Source value is used
* @requestField ?imageHeight | Number | Height to scale the screenshot to | >= 8, <= 4096 | Source value is used
* @requestField ?imageCompressionQuality | Number | Compression quality to use. 0 for high compression, 100 for uncompressed. -1 to use "default" (whatever that means, idk) | >= -1, <= 100 | -1
*
* @responseField imageData | String | Base64-encoded screenshot
*
* @requestType SaveSourceScreenshot
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category sources
*/
RequestResult RequestHandler::SaveSourceScreenshot(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
if (!(request.ValidateString("sourceName", statusCode, comment) && request.ValidateString("imageFilePath", statusCode, comment) && request.ValidateString("imageFormat", statusCode, comment)))
OBSSourceAutoRelease source = request.ValidateSource("sourceName", statusCode, comment);
if (!(source && request.ValidateString("imageFormat", statusCode, comment) && request.ValidateString("imageFilePath", statusCode, comment)))
return RequestResult::Error(statusCode, comment);
std::string sourceName = request.RequestData["sourceName"];
OBSSourceAutoRelease source = obs_get_source_by_name(sourceName.c_str());
if (!source)
return RequestResult::Error(RequestStatus::SourceNotFound);
if (obs_source_get_type(source) != OBS_SOURCE_TYPE_INPUT && obs_source_get_type(source) != OBS_SOURCE_TYPE_SCENE)
return RequestResult::Error(RequestStatus::InvalidSourceType, "The specified source is not an input or a scene.");
return RequestResult::Error(RequestStatus::InvalidResourceType, "The specified source is not an input or a scene.");
std::string imageFormat = request.RequestData["imageFormat"];
std::string imageFilePath = request.RequestData["imageFilePath"];
if (!IsImageFormatValid(imageFormat))
return RequestResult::Error(RequestStatus::InvalidRequestParameter, "Your specified image format is invalid or not supported by this system.");
return RequestResult::Error(RequestStatus::InvalidRequestField, "Your specified image format is invalid or not supported by this system.");
QFileInfo filePathInfo(QString::fromStdString(imageFilePath));
if (!filePathInfo.absoluteDir().exists())
return RequestResult::Error(RequestStatus::ResourceNotFound, "The directory for your file path does not exist.");
uint32_t requestedWidth{0};
uint32_t requestedHeight{0};
int compressionQuality{-1};
if (request.RequestData.contains("imageWidth") && !request.RequestData["imageWidth"].is_null()) {
if (!request.ValidateNumber("imageWidth", statusCode, comment, 8, 4096))
if (request.Contains("imageWidth")) {
if (!request.ValidateOptionalNumber("imageWidth", statusCode, comment, 8, 4096))
return RequestResult::Error(statusCode, comment);
requestedWidth = request.RequestData["imageWidth"];
}
if (request.RequestData.contains("imageHeight") && !request.RequestData["imageHeight"].is_null()) {
if (!request.ValidateNumber("imageHeight", statusCode, comment, 8, 4096))
if (request.Contains("imageHeight")) {
if (!request.ValidateOptionalNumber("imageHeight", statusCode, comment, 8, 4096))
return RequestResult::Error(statusCode, comment);
requestedHeight = request.RequestData["imageHeight"];
}
if (request.RequestData.contains("imageCompressionQuality") && !request.RequestData["imageCompressionQuality"].is_null()) {
if (!request.ValidateNumber("imageCompressionQuality", statusCode, comment, -1, 100))
if (request.Contains("imageCompressionQuality")) {
if (!request.ValidateOptionalNumber("imageCompressionQuality", statusCode, comment, -1, 100))
return RequestResult::Error(statusCode, comment);
compressionQuality = request.RequestData["imageCompressionQuality"];
@ -231,18 +303,48 @@ RequestResult RequestHandler::SaveSourceScreenshot(const Request& request)
QImage renderedImage = TakeSourceScreenshot(source, success, requestedWidth, requestedHeight);
if (!success)
return RequestResult::Error(RequestStatus::ScreenshotRenderFailed);
std::string imageFilePath = request.RequestData["imageFilePath"];
QFileInfo filePathInfo(QString::fromStdString(imageFilePath));
if (!filePathInfo.absoluteDir().exists())
return RequestResult::Error(RequestStatus::DirectoryNotFound, "The directory for your file path does not exist.");
return RequestResult::Error(RequestStatus::RequestProcessingFailed, "Failed to render screenshot.");
QString absoluteFilePath = filePathInfo.absoluteFilePath();
if (!renderedImage.save(absoluteFilePath, imageFormat.c_str(), compressionQuality))
return RequestResult::Error(RequestStatus::ScreenshotSaveFailed);
return RequestResult::Error(RequestStatus::RequestProcessingFailed, "Failed to save screenshot.");
return RequestResult::Success();
}
// Intentionally undocumented
RequestResult RequestHandler::GetSourcePrivateSettings(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSourceAutoRelease source = request.ValidateSource("sourceName", statusCode, comment);
if (!source)
return RequestResult::Error(statusCode, comment);
OBSDataAutoRelease privateSettings = obs_source_get_private_settings(source);
json responseData;
responseData["sourceSettings"] = Utils::Json::ObsDataToJson(privateSettings);
return RequestResult::Success(responseData);
}
// Intentionally undocumented
RequestResult RequestHandler::SetSourcePrivateSettings(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSourceAutoRelease source = request.ValidateSource("sourceName", statusCode, comment);
if (!source || !request.ValidateObject("sourceSettings", statusCode, comment))
return RequestResult::Error(statusCode, comment);
OBSDataAutoRelease privateSettings = obs_source_get_private_settings(source);
OBSDataAutoRelease newSettings = Utils::Json::JsonToObsData(request.RequestData["sourceSettings"]);
// Always overlays to prevent destroying internal source data unintentionally
obs_data_apply(privateSettings, newSettings);
return RequestResult::Success();
}

View File

@ -1,15 +1,53 @@
#include "RequestHandler.h"
#include "../plugin-macros.generated.h"
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
RequestResult RequestHandler::GetStreamStatus(const Request& request)
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#include "RequestHandler.h"
/**
* Gets the status of the stream output.
*
* @responseField outputActive | Boolean | Whether the output is active
* @responseField outputReconnecting | Boolean | Whether the output is currently reconnecting
* @responseField outputTimecode | String | Current formatted timecode string for the output
* @responseField outputDuration | Number | Current duration in milliseconds for the output
* @responseField outputBytes | Number | Number of bytes sent by the output
* @responseField outputSkippedFrames | Number | Number of frames skipped by the output's process
* @responseField outputTotalFrames | Number | Total number of frames delivered by the output's process
*
* @requestType GetStreamStatus
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category stream
*/
RequestResult RequestHandler::GetStreamStatus(const Request&)
{
OBSOutputAutoRelease streamOutput = obs_frontend_get_streaming_output();
uint64_t outputDuration = Utils::Obs::NumberHelper::GetOutputDuration(streamOutput);
json responseData;
responseData["outputActive"] = obs_output_active(streamOutput);
responseData["outputReconnecting"] = obs_output_reconnecting(streamOutput);
responseData["outputTimecode"] = Utils::Obs::StringHelper::GetOutputTimecodeString(streamOutput);
responseData["outputDuration"] = Utils::Obs::NumberHelper::GetOutputDuration(streamOutput);
responseData["outputTimecode"] = Utils::Obs::StringHelper::DurationToTimecode(outputDuration);
responseData["outputDuration"] = outputDuration;
responseData["outputBytes"] = (uint64_t)obs_output_get_total_bytes(streamOutput);
responseData["outputSkippedFrames"] = obs_output_get_frames_dropped(streamOutput);
responseData["outputTotalFrames"] = obs_output_get_total_frames(streamOutput);
@ -17,10 +55,46 @@ RequestResult RequestHandler::GetStreamStatus(const Request& request)
return RequestResult::Success(responseData);
}
RequestResult RequestHandler::StartStream(const Request& request)
/**
* Toggles the status of the stream output.
*
* @responseField outputActive | Boolean | New state of the stream output
*
* @requestType ToggleStream
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category stream
*/
RequestResult RequestHandler::ToggleStream(const Request&)
{
json responseData;
if (obs_frontend_streaming_active()) {
obs_frontend_streaming_stop();
responseData["outputActive"] = false;
} else {
obs_frontend_streaming_start();
responseData["outputActive"] = true;
}
return RequestResult::Success(responseData);
}
/**
* Starts the stream output.
*
* @requestType StartStream
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category stream
*/
RequestResult RequestHandler::StartStream(const Request&)
{
if (obs_frontend_streaming_active())
return RequestResult::Error(RequestStatus::StreamRunning);
return RequestResult::Error(RequestStatus::OutputRunning);
// TODO: Call signal directly to perform blocking wait
obs_frontend_streaming_start();
@ -28,13 +102,55 @@ RequestResult RequestHandler::StartStream(const Request& request)
return RequestResult::Success();
}
RequestResult RequestHandler::StopStream(const Request& request)
/**
* Stops the stream output.
*
* @requestType StopStream
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category stream
*/
RequestResult RequestHandler::StopStream(const Request&)
{
if (!obs_frontend_streaming_active())
return RequestResult::Error(RequestStatus::StreamNotRunning);
return RequestResult::Error(RequestStatus::OutputNotRunning);
// TODO: Call signal directly to perform blocking wait
obs_frontend_streaming_stop();
return RequestResult::Success();
}
/**
* Sends CEA-608 caption text over the stream output.
*
* @requestField captionText | String | Caption text
*
* @requestType SendStreamCaption
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @category stream
* @api requests
*/
RequestResult RequestHandler::SendStreamCaption(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
if (!request.ValidateString("captionText", statusCode, comment, true))
return RequestResult::Error(statusCode, comment);
if (!obs_frontend_streaming_active())
return RequestResult::Error(RequestStatus::OutputNotRunning);
std::string captionText = request.RequestData["captionText"];
OBSOutputAutoRelease output = obs_frontend_get_streaming_output();
// 0.0 means no delay until the next caption can be sent
obs_output_output_caption_text2(output, captionText.c_str(), 0.0);
return RequestResult::Success();
}

View File

@ -0,0 +1,322 @@
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#include <math.h>
#include "RequestHandler.h"
/**
* Gets an array of all available transition kinds.
*
* Similar to `GetInputKindList`
*
* @responseField transitionKinds | Array<String> | Array of transition kinds
*
* @requestType GetTransitionKindList
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category transitions
*/
RequestResult RequestHandler::GetTransitionKindList(const Request&)
{
json responseData;
responseData["transitionKinds"] = Utils::Obs::ArrayHelper::GetTransitionKindList();
return RequestResult::Success(responseData);
}
/**
* Gets an array of all scene transitions in OBS.
*
* @responseField currentSceneTransitionName | String | Name of the current scene transition. Can be null
* @responseField currentSceneTransitionKind | String | Kind of the current scene transition. Can be null
* @responseField transitions | Array<Object> | Array of transitions
*
* @requestType GetSceneTransitionList
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category transitions
*/
RequestResult RequestHandler::GetSceneTransitionList(const Request&)
{
json responseData;
OBSSourceAutoRelease transition = obs_frontend_get_current_transition();
if (transition) {
responseData["currentSceneTransitionName"] = obs_source_get_name(transition);
responseData["currentSceneTransitionKind"] = obs_source_get_id(transition);
} else {
responseData["currentSceneTransitionName"] = nullptr;
responseData["currentSceneTransitionKind"] = nullptr;
}
responseData["transitions"] = Utils::Obs::ArrayHelper::GetSceneTransitionList();
return RequestResult::Success(responseData);
}
/**
* Gets information about the current scene transition.
*
* @responseField transitionName | String | Name of the transition
* @responseField transitionKind | String | Kind of the transition
* @responseField transitionFixed | Boolean | Whether the transition uses a fixed (unconfigurable) duration
* @responseField transitionDuration | Number | Configured transition duration in milliseconds. `null` if transition is fixed
* @responseField transitionConfigurable | Boolean | Whether the transition supports being configured
* @responseField transitionSettings | Object | Object of settings for the transition. `null` if transition is not configurable
*
* @requestType GetCurrentSceneTransition
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category transitions
*/
RequestResult RequestHandler::GetCurrentSceneTransition(const Request&)
{
OBSSourceAutoRelease transition = obs_frontend_get_current_transition();
if (!transition)
return RequestResult::Error(RequestStatus::InvalidResourceState, "OBS does not currently have a scene transition set."); // This should not happen!
json responseData;
responseData["transitionName"] = obs_source_get_name(transition);
responseData["transitionKind"] = obs_source_get_id(transition);
if (obs_transition_fixed(transition)) {
responseData["transitionFixed"] = true;
responseData["transitionDuration"] = nullptr;
} else {
responseData["transitionFixed"] = false;
responseData["transitionDuration"] = obs_frontend_get_transition_duration();
}
if (obs_source_configurable(transition)) {
responseData["transitionConfigurable"] = true;
OBSDataAutoRelease transitionSettings = obs_source_get_settings(transition);
responseData["transitionSettings"] = Utils::Json::ObsDataToJson(transitionSettings);
} else {
responseData["transitionConfigurable"] = false;
responseData["transitionSettings"] = nullptr;
}
return RequestResult::Success(responseData);
}
/**
* Sets the current scene transition.
*
* Small note: While the namespace of scene transitions is generally unique, that uniqueness is not a guarantee as it is with other resources like inputs.
*
* @requestField transitionName | String | Name of the transition to make active
*
* @requestType SetCurrentSceneTransition
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category transitions
*/
RequestResult RequestHandler::SetCurrentSceneTransition(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
if (!request.ValidateString("transitionName", statusCode, comment))
return RequestResult::Error(statusCode, comment);
std::string transitionName = request.RequestData["transitionName"];
OBSSourceAutoRelease transition = Utils::Obs::SearchHelper::GetSceneTransitionByName(transitionName);
if (!transition)
return RequestResult::Error(RequestStatus::ResourceNotFound, "No scene transition was found by that name.");
obs_frontend_set_current_transition(transition);
return RequestResult::Success();
}
/**
* Sets the duration of the current scene transition, if it is not fixed.
*
* @requestField transitionDuration | Number | Duration in milliseconds | >= 50, <= 20000
*
* @requestType SetCurrentSceneTransitionDuration
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category transitions
*/
RequestResult RequestHandler::SetCurrentSceneTransitionDuration(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
if (!request.ValidateNumber("transitionDuration", statusCode, comment, 50, 20000))
return RequestResult::Error(statusCode, comment);
int transitionDuration = request.RequestData["transitionDuration"];
obs_frontend_set_transition_duration(transitionDuration);
return RequestResult::Success();
}
/**
* Sets the settings of the current scene transition.
*
* @requestField transitionSettings | Object | Settings object to apply to the transition. Can be `{}`
* @requestField ?overlay | Boolean | Whether to overlay over the current settings or replace them | true
*
* @requestType SetCurrentSceneTransitionSettings
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category transitions
*/
RequestResult RequestHandler::SetCurrentSceneTransitionSettings(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
if (!request.ValidateObject("transitionSettings", statusCode, comment, true))
return RequestResult::Error(statusCode, comment);
OBSSourceAutoRelease transition = obs_frontend_get_current_transition();
if (!transition)
return RequestResult::Error(RequestStatus::InvalidResourceState, "OBS does not currently have a scene transition set."); // This should not happen!
if (!obs_source_configurable(transition))
return RequestResult::Error(RequestStatus::ResourceNotConfigurable, "The current transition does not support custom settings.");
bool overlay = true;
if (request.Contains("overlay")) {
if (!request.ValidateOptionalBoolean("overlay", statusCode, comment))
return RequestResult::Error(statusCode, comment);
overlay = request.RequestData["overlay"];
}
OBSDataAutoRelease newSettings = Utils::Json::JsonToObsData(request.RequestData["transitionSettings"]);
if (!newSettings)
return RequestResult::Error(RequestStatus::RequestProcessingFailed, "An internal data conversion operation failed. Please report this!");
if (overlay)
obs_source_update(transition, newSettings);
else
obs_source_reset_settings(transition, newSettings);
obs_source_update_properties(transition);
return RequestResult::Success();
}
/**
* Gets the cursor position of the current scene transition.
*
* Note: `transitionCursor` will return 1.0 when the transition is inactive.
*
* @responseField transitionCursor | Number | Cursor position, between 0.0 and 1.0
*
* @requestType GetCurrentSceneTransitionCursor
* @complexity 2
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category transitions
*/
RequestResult RequestHandler::GetCurrentSceneTransitionCursor(const Request&)
{
OBSSourceAutoRelease transition = obs_frontend_get_current_transition();
if (!transition)
return RequestResult::Error(RequestStatus::InvalidResourceState, "OBS does not currently have a scene transition set."); // This should not happen!
json responseData;
responseData["transitionCursor"] = obs_transition_get_time(transition);
return RequestResult::Success(responseData);
}
/**
* Triggers the current scene transition. Same functionality as the `Transition` button in studio mode.
*
* @requestType TriggerStudioModeTransition
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category transitions
*/
RequestResult RequestHandler::TriggerStudioModeTransition(const Request&)
{
if (!obs_frontend_preview_program_mode_active())
return RequestResult::Error(RequestStatus::StudioModeNotActive);
OBSSourceAutoRelease previewScene = obs_frontend_get_current_preview_scene();
obs_frontend_set_current_scene(previewScene);
return RequestResult::Success();
}
/**
* Sets the position of the TBar.
*
* **Very important note**: This will be deprecated and replaced in a future version of obs-websocket.
*
* @requestField position | Number | New position | >= 0.0, <= 1.0
* @requestField ?release | Boolean | Whether to release the TBar. Only set `false` if you know that you will be sending another position update | `true`
*
* @requestType SetTBarPosition
* @complexity 3
* @rpcVersion -1
* @initialVersion 5.0.0
* @api requests
* @category transitions
*/
RequestResult RequestHandler::SetTBarPosition(const Request& request)
{
if (!obs_frontend_preview_program_mode_active())
return RequestResult::Error(RequestStatus::StudioModeNotActive);
RequestStatus::RequestStatus statusCode;
std::string comment;
if (!request.ValidateNumber("position", statusCode, comment, 0.0, 1.0))
return RequestResult::Error(statusCode, comment);
bool release = true;
if (request.Contains("release")) {
if (!request.ValidateOptionalBoolean("release", statusCode, comment))
return RequestResult::Error(statusCode, comment);
}
OBSSourceAutoRelease transition = obs_frontend_get_current_transition();
if (!transition)
return RequestResult::Error(RequestStatus::InvalidResourceState, "OBS does not currently have a scene transition set."); // This should not happen!
float position = request.RequestData["position"];
obs_frontend_set_tbar_position((int)round(position * 1024.0));
if (release)
obs_frontend_release_tbar();
return RequestResult::Success();
}

View File

@ -0,0 +1,150 @@
/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#include "RequestHandler.h"
/**
* Gets whether studio is enabled.
*
* @responseField studioModeEnabled | Boolean | Whether studio mode is enabled
*
* @requestType GetStudioModeEnabled
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category ui
* @api requests
*/
RequestResult RequestHandler::GetStudioModeEnabled(const Request&)
{
json responseData;
responseData["studioModeEnabled"] = obs_frontend_preview_program_mode_active();
return RequestResult::Success(responseData);
}
/**
* Enables or disables studio mode
*
* @requestField studioModeEnabled | Boolean | True == Enabled, False == Disabled
*
* @requestType SetStudioModeEnabled
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category ui
* @api requests
*/
RequestResult RequestHandler::SetStudioModeEnabled(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
if (!request.ValidateBoolean("studioModeEnabled", statusCode, comment))
return RequestResult::Error(statusCode, comment);
// Avoid queueing tasks if nothing will change
if (obs_frontend_preview_program_mode_active() != request.RequestData["studioModeEnabled"]) {
// (Bad) Create a boolean then pass it as a reference to the task. Requires `wait` in obs_queue_task() to be true, else undefined behavior
bool studioModeEnabled = request.RequestData["studioModeEnabled"];
// Queue the task inside of the UI thread to prevent race conditions
obs_queue_task(OBS_TASK_UI, [](void* param) {
auto studioModeEnabled = (bool*)param;
obs_frontend_set_preview_program_mode(*studioModeEnabled);
}, &studioModeEnabled, true);
}
return RequestResult::Success();
}
/**
* Opens the properties dialog of an input.
*
* @requestField inputName | String | Name of the input to open the dialog of
*
* @requestType OpenInputPropertiesDialog
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category ui
* @api requests
*/
RequestResult RequestHandler::OpenInputPropertiesDialog(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSourceAutoRelease input = request.ValidateInput("inputName", statusCode, comment);
if (!input)
return RequestResult::Error(statusCode, comment);
obs_frontend_open_source_properties(input);
return RequestResult::Success();
}
/**
* Opens the filters dialog of an input.
*
* @requestField inputName | String | Name of the input to open the dialog of
*
* @requestType OpenInputFiltersDialog
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category ui
* @api requests
*/
RequestResult RequestHandler::OpenInputFiltersDialog(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSourceAutoRelease input = request.ValidateInput("inputName", statusCode, comment);
if (!input)
return RequestResult::Error(statusCode, comment);
obs_frontend_open_source_filters(input);
return RequestResult::Success();
}
/**
* Opens the interact dialog of an input.
*
* @requestField inputName | String | Name of the input to open the dialog of
*
* @requestType OpenInputInteractDialog
* @complexity 1
* @rpcVersion -1
* @initialVersion 5.0.0
* @category ui
* @api requests
*/
RequestResult RequestHandler::OpenInputInteractDialog(const Request& request)
{
RequestStatus::RequestStatus statusCode;
std::string comment;
OBSSourceAutoRelease input = request.ValidateInput("inputName", statusCode, comment);
if (!input)
return RequestResult::Error(statusCode, comment);
if (!(obs_source_get_output_flags(input) & OBS_SOURCE_INTERACTION))
return RequestResult::Error(RequestStatus::InvalidResourceState, "The specified input does not support interaction.");
obs_frontend_open_source_interaction(input);
return RequestResult::Success();
}

Some files were not shown because too many files have changed in this diff Show More