obsws 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +0 -0
- data/README.md +116 -0
- data/lib/obsws/base.rb +117 -0
- data/lib/obsws/error.rb +6 -0
- data/lib/obsws/event.rb +92 -0
- data/lib/obsws/mixin.rb +53 -0
- data/lib/obsws/req.rb +888 -0
- data/lib/obsws/util.rb +16 -0
- data/lib/obsws/version.rb +25 -0
- data/lib/obsws.rb +11 -0
- metadata +151 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 339b5706222511d0cf272eff7040f45dc5bc1cffdfdc6b6d0cc59ea299c325e1
|
4
|
+
data.tar.gz: 2070c51a27e764297d2555bd958411fd8ee06c49f98bd09fa6144f7fba5790c8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c80761585030c73bd304de37ead5ea8f5260116f7e2acb6a15549d920695be579c0fc0ffa9f0485569ffd56f4c11b6ca027711eeadb2ecb81c8b9b3016e68396
|
7
|
+
data.tar.gz: 13fc659eb70544b51555cb9f68608927f64671377f550ae78c3d9a83b60de2f42050d17403ae72aeb701525fc15e48a5086d37f05c780b4055a24cfe252bd0a1
|
data/CHANGELOG.md
ADDED
File without changes
|
data/README.md
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
[](https://github.com/onyx-and-iris/voicemeeter-api-ruby/blob/dev/LICENSE)
|
2
|
+
[](https://github.com/prettier/plugin-ruby)
|
3
|
+
|
4
|
+
# A Ruby wrapper around OBS Studio WebSocket v5.0
|
5
|
+
|
6
|
+
## Requirements
|
7
|
+
|
8
|
+
- [OBS Studio](https://obsproject.com/)
|
9
|
+
- [OBS Websocket v5 Plugin](https://github.com/obsproject/obs-websocket/releases/tag/5.0.0)
|
10
|
+
- With the release of OBS Studio version 28, Websocket plugin is included by default. But it should be manually installed for earlier versions of OBS.
|
11
|
+
- Ruby 3.0 or greater
|
12
|
+
|
13
|
+
## `Use`
|
14
|
+
|
15
|
+
#### Example `main.rb`
|
16
|
+
|
17
|
+
pass `host`, `port` and `password` as keyword arguments.
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
require_relative "lib/obsws"
|
21
|
+
|
22
|
+
def main
|
23
|
+
r_client =
|
24
|
+
OBSWS::Requests::Client.new(
|
25
|
+
host: "localhost",
|
26
|
+
port: 4455,
|
27
|
+
password: "strongpassword"
|
28
|
+
)
|
29
|
+
|
30
|
+
r_client.run do
|
31
|
+
# Toggle the mute state of your Mic input
|
32
|
+
r_client.toggle_input_mute("Mic/Aux")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
main if $0 == __FILE__
|
37
|
+
```
|
38
|
+
|
39
|
+
### Requests
|
40
|
+
|
41
|
+
Method names for requests match the API calls but snake cased. `run` accepts a block that closes the socket once you are done.
|
42
|
+
|
43
|
+
example:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
r_client.run do
|
47
|
+
# GetVersion
|
48
|
+
resp = r_client.get_version
|
49
|
+
|
50
|
+
# SetCurrentProgramScene
|
51
|
+
r_client.set_current_program_scene("BRB")
|
52
|
+
end
|
53
|
+
```
|
54
|
+
|
55
|
+
For a full list of requests refer to [Requests](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#requests)
|
56
|
+
|
57
|
+
### Events
|
58
|
+
|
59
|
+
Register an observer class and define `on_` methods for events. Method names should match the api event but snake cased.
|
60
|
+
|
61
|
+
example:
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
class Observer
|
65
|
+
def initialize
|
66
|
+
@e_client = OBSWS::Events::Client.new(**kwargs)
|
67
|
+
# register class with the event client
|
68
|
+
@e_client.add_observer(self)
|
69
|
+
end
|
70
|
+
|
71
|
+
# define "on_" event methods.
|
72
|
+
def on_current_program_scene_changed
|
73
|
+
...
|
74
|
+
end
|
75
|
+
def on_input_mute_state_changed
|
76
|
+
...
|
77
|
+
end
|
78
|
+
...
|
79
|
+
end
|
80
|
+
```
|
81
|
+
|
82
|
+
For a full list of events refer to [Events](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#events)
|
83
|
+
|
84
|
+
### Attributes
|
85
|
+
|
86
|
+
For both request responses and event data you may inspect the available attributes using `attrs`.
|
87
|
+
|
88
|
+
example:
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
resp = cl.get_version
|
92
|
+
p resp.attrs
|
93
|
+
|
94
|
+
def on_scene_created(data):
|
95
|
+
p data.attrs
|
96
|
+
```
|
97
|
+
|
98
|
+
### Errors
|
99
|
+
|
100
|
+
If a request fails an `OBSWSError` will be raised with a status code.
|
101
|
+
|
102
|
+
For a full list of status codes refer to [Codes](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#requeststatus)
|
103
|
+
|
104
|
+
### Tests
|
105
|
+
|
106
|
+
To run all tests:
|
107
|
+
|
108
|
+
```
|
109
|
+
bundle exec rake -v
|
110
|
+
```
|
111
|
+
|
112
|
+
### Official Documentation
|
113
|
+
|
114
|
+
For the full documentation:
|
115
|
+
|
116
|
+
- [OBS Websocket SDK](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#obs-websocket-501-protocol)
|
data/lib/obsws/base.rb
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
require "socket"
|
2
|
+
require "websocket/driver"
|
3
|
+
require "digest/sha2"
|
4
|
+
require "json"
|
5
|
+
require "observer"
|
6
|
+
require "waitutil"
|
7
|
+
|
8
|
+
require_relative "mixin"
|
9
|
+
require_relative "error"
|
10
|
+
|
11
|
+
module OBSWS
|
12
|
+
class Socket
|
13
|
+
attr_reader :url
|
14
|
+
|
15
|
+
def initialize(url, socket)
|
16
|
+
@url = url
|
17
|
+
@socket = socket
|
18
|
+
end
|
19
|
+
|
20
|
+
def write(s)
|
21
|
+
@socket.write(s)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class Base
|
26
|
+
include Observable
|
27
|
+
include Mixin::OPCodes
|
28
|
+
|
29
|
+
attr_reader :id, :driver, :closed
|
30
|
+
|
31
|
+
def initialize(**kwargs)
|
32
|
+
host = kwargs[:host] || "localhost"
|
33
|
+
port = kwargs[:port] || 4455
|
34
|
+
@password = kwargs[:password] || ""
|
35
|
+
@subs = kwargs[:subs] || 0
|
36
|
+
|
37
|
+
@socket = TCPSocket.new(host, port)
|
38
|
+
@driver =
|
39
|
+
WebSocket::Driver.client(Socket.new("ws://#{host}:#{port}", @socket))
|
40
|
+
@ready = false
|
41
|
+
@closed = false
|
42
|
+
@driver.on :open do |msg|
|
43
|
+
LOGGER.debug("driver socket open")
|
44
|
+
@ready = true
|
45
|
+
end
|
46
|
+
@driver.on :close do |msg|
|
47
|
+
LOGGER.debug("driver socket closed")
|
48
|
+
@closed = true
|
49
|
+
end
|
50
|
+
@driver.on :message do |msg|
|
51
|
+
LOGGER.debug("received [#{msg}] passing to handler")
|
52
|
+
msg_handler(JSON.parse(msg.data, symbolize_names: true))
|
53
|
+
end
|
54
|
+
Thread.new { start_driver }
|
55
|
+
WaitUtil.wait_for_condition(
|
56
|
+
"driver socket ready",
|
57
|
+
delay_sec: 0.01,
|
58
|
+
timeout_sec: 0.5
|
59
|
+
) { @ready }
|
60
|
+
end
|
61
|
+
|
62
|
+
def start_driver
|
63
|
+
@driver.start
|
64
|
+
|
65
|
+
loop do
|
66
|
+
@driver.parse(@socket.readpartial(4096))
|
67
|
+
rescue EOFError
|
68
|
+
break
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def auth_token(salt:, challenge:)
|
73
|
+
Digest::SHA256.base64digest(
|
74
|
+
Digest::SHA256.base64digest(@password + salt) + challenge
|
75
|
+
)
|
76
|
+
end
|
77
|
+
|
78
|
+
def authenticate(auth)
|
79
|
+
token = auth_token(**auth)
|
80
|
+
payload = {
|
81
|
+
op: Mixin::OPCodes::IDENTIFY,
|
82
|
+
d: {
|
83
|
+
rpcVersion: 1,
|
84
|
+
authentication: token,
|
85
|
+
eventSubscriptions: @subs
|
86
|
+
}
|
87
|
+
}
|
88
|
+
@driver.text(JSON.generate(payload))
|
89
|
+
end
|
90
|
+
|
91
|
+
def msg_handler(data)
|
92
|
+
op_code = data[:op]
|
93
|
+
case op_code
|
94
|
+
when Mixin::OPCodes::HELLO
|
95
|
+
authenticate(data[:d][:authentication])
|
96
|
+
when Mixin::OPCodes::IDENTIFIED
|
97
|
+
LOGGER.debug("Authentication successful")
|
98
|
+
when Mixin::OPCodes::EVENT, Mixin::OPCodes::REQUESTRESPONSE
|
99
|
+
changed
|
100
|
+
notify_observers(op_code, data[:d])
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def req(id, type_, data = nil)
|
105
|
+
payload = {
|
106
|
+
op: Mixin::OPCodes::REQUEST,
|
107
|
+
d: {
|
108
|
+
requestType: type_,
|
109
|
+
requestId: id
|
110
|
+
}
|
111
|
+
}
|
112
|
+
payload[:d][:requestData] = data if data
|
113
|
+
queued = @driver.text(JSON.generate(payload))
|
114
|
+
LOGGER.debug("request with id #{id} queued? #{queued}")
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
data/lib/obsws/error.rb
ADDED
data/lib/obsws/event.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
require "json"
|
2
|
+
|
3
|
+
require_relative "util"
|
4
|
+
require_relative "mixin"
|
5
|
+
|
6
|
+
module OBSWS
|
7
|
+
module Events
|
8
|
+
module SUBS
|
9
|
+
NONE = 0
|
10
|
+
GENERAL = (1 << 0)
|
11
|
+
CONFIG = (1 << 1)
|
12
|
+
SCENES = (1 << 2)
|
13
|
+
INPUTS = (1 << 3)
|
14
|
+
TRANSITIONS = (1 << 4)
|
15
|
+
FILTERS = (1 << 5)
|
16
|
+
OUTPUTS = (1 << 6)
|
17
|
+
SCENEITEMS = (1 << 7)
|
18
|
+
MEDIAINPUTS = (1 << 8)
|
19
|
+
VENDORS = (1 << 9)
|
20
|
+
UI = (1 << 10)
|
21
|
+
|
22
|
+
def low_volume
|
23
|
+
GENERAL | CONFIG | SCENES | INPUTS | TRANSITIONS | FILTERS | OUTPUTS |
|
24
|
+
SCENEITEMS | MEDIAINPUTS | VENDORS | UI
|
25
|
+
end
|
26
|
+
|
27
|
+
INPUTVOLUMEMETERS = (1 << 16)
|
28
|
+
INPUTACTIVESTATECHANGED = (1 << 17)
|
29
|
+
INPUTSHOWSTATECHANGED = (1 << 18)
|
30
|
+
SCENEITEMTRANSFORMCHANGED = (1 << 19)
|
31
|
+
|
32
|
+
def high_volume
|
33
|
+
INPUTVOLUMEMETERS | INPUTACTIVESTATECHANGED | INPUTSHOWSTATECHANGED |
|
34
|
+
SCENEITEMTRANSFORMCHANGED
|
35
|
+
end
|
36
|
+
|
37
|
+
def all
|
38
|
+
low_volume | high_volume
|
39
|
+
end
|
40
|
+
|
41
|
+
module_function :low_volume, :high_volume, :all
|
42
|
+
end
|
43
|
+
|
44
|
+
module Callbacks
|
45
|
+
include Util
|
46
|
+
|
47
|
+
def add_observer(observer)
|
48
|
+
@observers = [] unless defined?(@observers)
|
49
|
+
observer = [observer] if !observer.respond_to? :each
|
50
|
+
observer.each { |o| @observers.append(o) }
|
51
|
+
end
|
52
|
+
|
53
|
+
def remove_observer(observer)
|
54
|
+
@observers.delete(observer)
|
55
|
+
end
|
56
|
+
|
57
|
+
def notify_observers(event, data)
|
58
|
+
if defined?(@observers)
|
59
|
+
@observers.each do |o|
|
60
|
+
if o.respond_to? "on_#{event.to_snake}"
|
61
|
+
if data.empty?
|
62
|
+
o.send("on_#{event.to_snake}")
|
63
|
+
else
|
64
|
+
o.send("on_#{event.to_snake}", data)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
class Client
|
73
|
+
include Callbacks
|
74
|
+
include Mixin::TearDown
|
75
|
+
include Mixin::OPCodes
|
76
|
+
|
77
|
+
def initialize(**kwargs)
|
78
|
+
kwargs[:subs] = SUBS.low_volume
|
79
|
+
@base_client = Base.new(**kwargs)
|
80
|
+
@base_client.add_observer(self)
|
81
|
+
end
|
82
|
+
|
83
|
+
def update(op_code, data)
|
84
|
+
if op_code == Mixin::OPCodes::EVENT
|
85
|
+
event = data[:eventType]
|
86
|
+
data = data.key?(:eventData) ? data[:eventData] : {}
|
87
|
+
notify_observers(event, Mixin::Data.new(data, data.keys))
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
data/lib/obsws/mixin.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require_relative "util"
|
2
|
+
|
3
|
+
module OBSWS
|
4
|
+
module Mixin
|
5
|
+
module Meta
|
6
|
+
include Util
|
7
|
+
|
8
|
+
def make_response_methods(*params)
|
9
|
+
params.each do |param|
|
10
|
+
define_singleton_method(param.to_s.to_snake) { @resp[param] }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class MetaObject
|
16
|
+
include Mixin::Meta
|
17
|
+
|
18
|
+
def initialize(resp, fields)
|
19
|
+
@resp = resp
|
20
|
+
@fields = fields
|
21
|
+
self.make_response_methods *fields
|
22
|
+
end
|
23
|
+
|
24
|
+
def empty? = @fields.empty?
|
25
|
+
|
26
|
+
def attrs = @fields.map { |f| f.to_s.to_snake }
|
27
|
+
end
|
28
|
+
|
29
|
+
class Response < MetaObject
|
30
|
+
end
|
31
|
+
|
32
|
+
class Data < MetaObject
|
33
|
+
end
|
34
|
+
|
35
|
+
module TearDown
|
36
|
+
def close
|
37
|
+
@base_client.driver.close
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
module OPCodes
|
42
|
+
HELLO = 0
|
43
|
+
IDENTIFY = 1
|
44
|
+
IDENTIFIED = 2
|
45
|
+
REIDENTIFY = 3
|
46
|
+
EVENT = 5
|
47
|
+
REQUEST = 6
|
48
|
+
REQUESTRESPONSE = 7
|
49
|
+
REQUESTBATCH = 8
|
50
|
+
REQUESTBATCHRESPONSE = 9
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|