obsws 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/onyx-and-iris/voicemeeter-api-ruby/blob/dev/LICENSE)
|
2
|
+
[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](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
|