obsws 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,6 @@
1
+ module OBSWS
2
+ module Error
3
+ class OBSWSError < StandardError
4
+ end
5
+ end
6
+ end
@@ -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
@@ -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