Compare commits

54 Commits

Author SHA1 Message Date
daa8c6ada1 adds Events::Client.on method
allows registering blocks to be called back later

examples, readme updated

minor bump
2023-08-17 23:09:32 +01:00
662f14282f move identified before start_driver 2023-08-17 16:28:43 +01:00
2e0e584d3a refactor 2023-08-17 16:18:50 +01:00
a8425cf7cd refactor 2023-08-17 16:13:57 +01:00
7e580dc91a check identified state in Event::Client
add passwordless test for event client

patch bump
2023-08-11 22:12:28 +01:00
92174219a7 patch bump 2023-08-11 22:05:28 +01:00
0c71eb2398 added no password error test 2023-08-11 22:05:14 +01:00
3ea21cd371 Identified class added. tracks identified state 2023-08-11 22:05:01 +01:00
70b60b3cac reword 2023-08-11 18:23:42 +01:00
c97d14abe2 move logger back into rescue block 2023-08-11 17:10:17 +01:00
123b9c55ca patch bump 2023-08-11 17:05:47 +01:00
299351cac0 refactor tests 2023-08-11 17:05:33 +01:00
3ef4396885 request ids are now UUID's. 2023-08-11 17:04:38 +01:00
c6bb8d07ff patch bump 2023-08-11 16:15:42 +01:00
48b94a2682 remove running reader method 2023-08-11 16:15:27 +01:00
210d13ba1e update main example to print mute state 2023-08-11 16:15:17 +01:00
59bcf2a338 assign timeout_sec directly 2023-08-11 16:14:53 +01:00
dc8ac155ec patch bump 2023-08-11 14:45:02 +01:00
23d64ef9d8 test files renamed with test_ prefix
error tests added

Rakefile updated with new test file names

event tasks moved into :e namespace
2023-08-11 14:44:51 +01:00
9be9dc80a2 adds connect_timeout kwarg for base class 2023-08-11 14:41:35 +01:00
a40ab77be9 cleanup error messages 2023-08-11 14:41:09 +01:00
b440ace20c fix req_name
patch bump
2023-08-11 13:18:28 +01:00
f5a817ab4e fix req_name
patch bump
2023-08-11 13:16:27 +01:00
13f57f79f6 rename name to req_name in OBSWSRequestError
patch bump
2023-08-11 13:10:44 +01:00
976c8f19a8 new error classes
OBSWSConnectionError, OBSWSRequestError added

they subclass OBSWSError

readme updated with new error classes
2023-08-11 02:22:14 +01:00
515fa565d4 use conditional assignment
patch bump
2023-08-06 11:33:15 +01:00
46bfb53db8 add docstring 2023-08-03 14:39:44 +01:00
f669498c69 remove_observers now accepts array of callbacks
callbacks aliases observers

patch bump
2023-08-03 14:15:46 +01:00
aeec0635ca add and control flow operator 2023-07-30 00:06:39 +01:00
86b84aeef9 minor syntax changes 2023-07-28 19:05:28 +01:00
e4f4961c56 upd Gemfile.lock 2023-07-27 15:08:33 +01:00
4fdebc8178 Callbacks module extended
Now supports callback methods as well observer classes

levels example now uses callback methods

register,deregister now alias add/remove observer methods

minor version bump
2023-07-27 14:55:00 +01:00
155cbe019a upd Gemfile.lock 2023-07-26 19:53:04 +01:00
6293ae7b8c patch bump 2023-07-26 18:55:57 +01:00
57fca646b5 remove the monkey patching 2023-07-26 18:55:35 +01:00
d12a1a5954 refactor Callbacks 2023-07-26 18:55:19 +01:00
438f3b1659 upd Gemfile.lock 2023-07-26 17:27:41 +01:00
d15418a660 mixin only methods for directing the driver
patch bump
2023-07-26 16:55:59 +01:00
2883fd42cc Socket class and driver methods
moved into Driver module

patch bump
2023-07-26 16:38:36 +01:00
88b2eabc0c typo fix 2023-07-26 16:15:43 +01:00
e15e17cc9f update readme title 2023-07-26 16:12:38 +01:00
72e09d5278 minor version bump 2023-07-26 14:38:12 +01:00
11d991b039 examples updated 2023-07-26 14:38:01 +01:00
3d3d8f3020 log level may now be set with environment variable 2023-07-26 14:37:49 +01:00
82c6ced760 logger module added 2023-07-26 14:37:35 +01:00
72ee539b96 upd gemfile.lock 2023-07-26 10:54:38 +01:00
bbfaf486c3 observer dependency removed
patchbump
2023-07-26 10:52:23 +01:00
8534c59fa2 close now aliases stop_driver 2023-07-26 10:52:07 +01:00
9940fbbf9f assign client updater methods as base lambdas 2023-07-26 10:51:38 +01:00
18d291c6eb rename info to infostring, now returns a string 2023-07-26 10:18:32 +01:00
6dc21314e8 break instead of exit 2023-07-21 07:55:50 +01:00
15585c90e9 fix error in rakefile 2023-07-21 06:39:53 +01:00
15c4baf5d7 raekfile updated
rework examples
2023-07-21 06:37:14 +01:00
15dcaeedda remove unnecessary assignment. 2023-07-21 06:04:09 +01:00
27 changed files with 384 additions and 397 deletions

View File

@@ -1,8 +1,7 @@
PATH PATH
remote: . remote: .
specs: specs:
obsws (0.2.0) obsws (0.5.8)
observer (~> 0.1.1)
waitutil (~> 0.2.1) waitutil (~> 0.2.1)
websocket-driver (~> 0.7.5) websocket-driver (~> 0.7.5)
@@ -14,7 +13,6 @@ GEM
language_server-protocol (3.17.0.3) language_server-protocol (3.17.0.3)
lint_roller (1.1.0) lint_roller (1.1.0)
minitest (5.16.3) minitest (5.16.3)
observer (0.1.1)
parallel (1.23.0) parallel (1.23.0)
parser (3.2.2.3) parser (3.2.2.3)
ast (~> 2.4.1) ast (~> 2.4.1)
@@ -54,7 +52,7 @@ GEM
rubocop-performance (~> 1.18.0) rubocop-performance (~> 1.18.0)
unicode-display_width (2.4.2) unicode-display_width (2.4.2)
waitutil (0.2.1) waitutil (0.2.1)
websocket-driver (0.7.5) websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5) websocket-extensions (0.1.5)

View File

@@ -2,7 +2,7 @@
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/onyx-and-iris/obsws-ruby/blob/dev/LICENSE) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/onyx-and-iris/obsws-ruby/blob/dev/LICENSE)
[![Ruby Code Style](https://img.shields.io/badge/code_style-standard-violet.svg)](https://github.com/standardrb/standard) [![Ruby Code Style](https://img.shields.io/badge/code_style-standard-violet.svg)](https://github.com/standardrb/standard)
# A Ruby wrapper around OBS Studio WebSocket v5.0 # Ruby Clients for OBS Studio WebSocket v5.0
## Requirements ## Requirements
@@ -16,75 +16,73 @@
### Bundler ### Bundler
``` ```
bundle add 'obsws' bundle add obsws
bundle install bundle install
``` ```
### Gem
`gem install 'obsws'`
## `Use` ## `Use`
#### Example `main.rb` #### Example `main.rb`
pass `host`, `port` and `password` as keyword arguments. Pass `host`, `port` and `password` as keyword arguments.
```ruby ```ruby
require "obsws" require "obsws"
def main class Main
INPUT = "Mic/Aux"
def run
OBSWS::Requests::Client OBSWS::Requests::Client
.new(host: "localhost", port: 4455, password: "strongpassword") .new(host: "localhost", port: 4455, password: "strongpassword")
.run do |client| .run do |client|
# Toggle the mute state of your Mic input # Toggle the mute state of your Mic input and print its new mute state
client.toggle_input_mute("Mic/Aux") client.toggle_input_mute(INPUT)
resp = client.get_input_mute(INPUT)
puts "Input '#{INPUT}' was set to #{resp.input_muted}"
end
end end
end end
main if $0 == __FILE__ Main.new.run if $PROGRAM_NAME == __FILE__
``` ```
Passing OBSWS::Requests::Client.run a block closes the socket once the block returns.
### Requests ### Requests
Method names for requests match the API calls but snake cased. `run` accepts a block that closes the socket once you are done. Method names for requests match the API calls but snake cased.
example: example:
```ruby ```ruby
r_client.run do # GetVersion
# GetVersion resp = r_client.get_version
resp = r_client.get_version
# SetCurrentProgramScene # SetCurrentProgramScene
r_client.set_current_program_scene("BRB") r_client.set_current_program_scene("BRB")
end
``` ```
For a full list of requests refer to [Requests](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#requests) For a full list of requests refer to [Requests](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#requests)
### Events ### Events
Register an observer class and define `on_` methods for events. Method names should match the api event but snake cased. Register blocks with the Event client using the `on` method. The event data will be passed to the block.
example: example:
```ruby ```ruby
class Observer class Observer
def initialize def initialize
@e_client = OBSWS::Events::Client.new(**kwargs) @e_client = OBSWS::Events::Client.new(host: "localhost", port: 4455, password: "strongpassword")
# register class with the event client # register blocks on event types.
@e_client.add_observer(self) @e_client.on(:current_program_scene_changed) do |data|
end
# define "on_" event methods.
def on_current_program_scene_changed
... ...
end end
def on_input_mute_state_changed @e_client.on(:input_mute_state_changed) do |data|
... ...
end end
... end
end end
``` ```
@@ -106,21 +104,26 @@ def on_scene_created(data):
### Errors ### Errors
If a request fails an `OBSWSError` will be raised with a status code. If a general error occurs an `OBSWSError` will be raised.
If a connection attempt fails or times out an `OBSWSConnectionError` will be raised.
If a request fails an `OBSWSRequestError` will be raised with a status code.
- The request name and code are retrievable through the following attributes:
- `req_name`
- `code`
For a full list of status codes refer to [Codes](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#requeststatus) For a full list of status codes refer to [Codes](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#requeststatus)
### Logging ### Logging
To see the raw messages set log level to debug To enable logs set an environmental variable `OBSWS_LOG_LEVEL` to the appropriate level.
example: example in powershell:
```ruby ```powershell
require "obsws" $env:OBSWS_LOG_LEVEL="DEBUG"
OBSWS::LOGGER.debug!
...
``` ```
### Tests ### Tests

View File

@@ -5,19 +5,25 @@ HERE = __dir__
Minitest::TestTask.create(:test) do |t| Minitest::TestTask.create(:test) do |t|
t.libs << "test" t.libs << "test"
t.warning = false t.warning = false
t.test_globs = ["test/**/*_test.rb"] t.test_globs = ["test/**/test_*.rb"]
end end
task default: :test task default: :test
task :events do
namespace :e do
desc "Runs the events example"
task :events do
filepath = File.join(HERE, "examples", "events", "main.rb") filepath = File.join(HERE, "examples", "events", "main.rb")
ruby filepath ruby filepath
end end
task :levels do desc "Runs the levels example"
task :levels do
filepath = File.join(HERE, "examples", "levels", "main.rb") filepath = File.join(HERE, "examples", "levels", "main.rb")
ruby filepath ruby filepath
end end
task :scene_rotate do desc "Runs the scene_rotate example"
task :scene_rotate do
filepath = File.join(HERE, "examples", "scene_rotate", "main.rb") filepath = File.join(HERE, "examples", "scene_rotate", "main.rb")
ruby filepath ruby filepath
end
end end

View File

@@ -1,7 +0,0 @@
# frozen_string_literal: true
source "https://rubygems.org"
# gem "rails"
gem "obsws", path: "../.."

View File

@@ -1,25 +0,0 @@
PATH
remote: ../..
specs:
obsws (0.1.3)
observer (~> 0.1.1)
waitutil (~> 0.2.1)
websocket-driver (~> 0.7.5)
GEM
remote: https://rubygems.org/
specs:
observer (0.1.1)
waitutil (0.2.1)
websocket-driver (0.7.5)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
PLATFORMS
x64-mingw-ucrt
DEPENDENCIES
obsws!
BUNDLED WITH
2.3.22

View File

@@ -1,56 +1,46 @@
require "obsws" require_relative "../../lib/obsws"
require "yaml" require "yaml"
OBSWS::LOGGER.info!
class Main class Main
attr_reader :running
def initialize(**kwargs) def initialize(**kwargs)
@r_client = OBSWS::Requests::Client.new(**kwargs) @r_client = OBSWS::Requests::Client.new(**kwargs)
@e_client = OBSWS::Events::Client.new(**kwargs) @e_client = OBSWS::Events::Client.new(**kwargs)
@e_client.add_observer(self)
puts info.join("\n") @e_client.on(:current_program_scene_changed) do |data|
@running = true
end
def run
sleep(0.1) while running
end
def info
resp = @r_client.get_version
[
"Using obs version:",
resp.obs_version,
"With websocket version:",
resp.obs_web_socket_version
]
end
def on_current_program_scene_changed(data)
puts "Switched to scene #{data.scene_name}" puts "Switched to scene #{data.scene_name}"
end end
@e_client.on(:scene_created) do |data|
def on_scene_created(data)
puts "scene #{data.scene_name} has been created" puts "scene #{data.scene_name} has been created"
end end
@e_client.on(:input_mute_state_changed) do |data|
def on_input_mute_state_changed(data)
puts "#{data.input_name} mute toggled" puts "#{data.input_name} mute toggled"
end end
@e_client.on(:exit_started) do
def on_exit_started
puts "OBS closing!" puts "OBS closing!"
@r_client.close @r_client.close
@e_client.close @e_client.close
@running = false @running = false
end end
puts infostring
end
def infostring
resp = @r_client.get_version
[
"Using obs version: #{resp.obs_version}.",
"With websocket version: #{resp.obs_web_socket_version}"
].join(" ")
end
def run
@running = true
sleep(0.1) while @running
end
end end
def conn_from_yaml def conn_from_yaml
YAML.load_file("obs.yml", symbolize_names: true)[:connection] YAML.load_file("obs.yml", symbolize_names: true)[:connection]
end end
Main.new(**conn_from_yaml).run if $0 == __FILE__ Main.new(**conn_from_yaml).run if $PROGRAM_NAME == __FILE__

View File

@@ -1,7 +0,0 @@
# frozen_string_literal: true
source "https://rubygems.org"
# gem "rails"
gem "obsws", path: "../.."

View File

@@ -1,25 +0,0 @@
PATH
remote: ../..
specs:
obsws (0.1.3)
observer (~> 0.1.1)
waitutil (~> 0.2.1)
websocket-driver (~> 0.7.5)
GEM
remote: https://rubygems.org/
specs:
observer (0.1.1)
waitutil (0.2.1)
websocket-driver (0.7.5)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
PLATFORMS
x64-mingw-ucrt
DEPENDENCIES
obsws!
BUNDLED WITH
2.4.15

View File

@@ -1,9 +1,6 @@
require "obsws" require_relative "../../lib/obsws"
require "yaml" require "yaml"
OBSWS::LOGGER.info!
DEVICE = "Desktop Audio"
module LevelTypes module LevelTypes
VU = 0 VU = 0
POSTFADER = 1 POSTFADER = 1
@@ -11,24 +8,18 @@ module LevelTypes
end end
class Main class Main
DEVICE = "Desktop Audio"
def initialize(**kwargs) def initialize(**kwargs)
subs = OBSWS::Events::SUBS::LOW_VOLUME | OBSWS::Events::SUBS::INPUTVOLUMEMETERS subs = OBSWS::Events::SUBS::LOW_VOLUME | OBSWS::Events::SUBS::INPUTVOLUMEMETERS
@e_client = OBSWS::Events::Client.new(subs:, **kwargs) @e_client = OBSWS::Events::Client.new(subs:, **kwargs)
@e_client.add_observer(self)
end
def run @e_client.on(:input_mute_state_changed) do |data|
puts "press <Enter> to quit"
exit if gets.chomp.empty?
end
def on_input_mute_state_changed(data)
if data.input_name == DEVICE if data.input_name == DEVICE
puts "#{DEVICE} mute toggled" puts "#{DEVICE} mute toggled"
end end
end end
@e_client.on(:input_volume_meters) do |data|
def on_input_volume_meters(data)
fget = ->(x) { (x > 0) ? (20 * Math.log(x, 10)).round(1) : -200.0 } fget = ->(x) { (x > 0) ? (20 * Math.log(x, 10)).round(1) : -200.0 }
data.inputs.each do |d| data.inputs.each do |d|
@@ -39,10 +30,16 @@ class Main
end end
end end
end end
end
def run
puts "press <Enter> to quit"
loop { break if gets.chomp.empty? }
end
end end
def conn_from_yaml def conn_from_yaml
YAML.load_file("obs.yml", symbolize_names: true)[:connection] YAML.load_file("obs.yml", symbolize_names: true)[:connection]
end end
Main.new(**conn_from_yaml).run if $0 == __FILE__ Main.new(**conn_from_yaml).run if $PROGRAM_NAME == __FILE__

View File

@@ -1,7 +0,0 @@
# frozen_string_literal: true
source "https://rubygems.org"
# gem "rails"
gem "obsws", path: "../.."

View File

@@ -1,25 +0,0 @@
PATH
remote: ../..
specs:
obsws (0.1.3)
observer (~> 0.1.1)
waitutil (~> 0.2.1)
websocket-driver (~> 0.7.5)
GEM
remote: https://rubygems.org/
specs:
observer (0.1.1)
waitutil (0.2.1)
websocket-driver (0.7.5)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
PLATFORMS
x64-mingw-ucrt
DEPENDENCIES
obsws!
BUNDLED WITH
2.3.22

View File

@@ -1,13 +1,13 @@
require "obsws" require_relative "../../lib/obsws"
require "yaml" require "yaml"
OBSWS::LOGGER.info!
def conn_from_yaml class Main
def conn_from_yaml
YAML.load_file("obs.yml", symbolize_names: true)[:connection] YAML.load_file("obs.yml", symbolize_names: true)[:connection]
end end
def main def run
OBSWS::Requests::Client.new(**conn_from_yaml).run do |client| OBSWS::Requests::Client.new(**conn_from_yaml).run do |client|
resp = client.get_scene_list resp = client.get_scene_list
resp.scenes.reverse_each do |scene| resp.scenes.reverse_each do |scene|
@@ -16,6 +16,8 @@ def main
sleep(0.5) sleep(0.5)
end end
end end
end
end end
main if $0 == __FILE__
Main.new.run if $PROGRAM_NAME == __FILE__

View File

@@ -1,11 +1,36 @@
require "digest/sha2"
require "json"
require "logger" require "logger"
require "securerandom"
require "socket"
require "waitutil"
require "websocket/driver"
require_relative "obsws/logger"
require_relative "obsws/driver"
require_relative "obsws/util"
require_relative "obsws/mixin"
require_relative "obsws/base"
require_relative "obsws/req" require_relative "obsws/req"
require_relative "obsws/event" require_relative "obsws/event"
module OBSWS require_relative "obsws/version"
include Logger::Severity
LOGGER = Logger.new(STDOUT) module OBSWS
LOGGER.level = WARN class OBSWSError < StandardError; end
class OBSWSConnectionError < OBSWSError; end
class OBSWSRequestError < OBSWSError
attr_reader :req_name, :code
def initialize(req_name, code, comment)
@req_name = req_name
@code = code
message = "Request #{@req_name} returned code #{@code}."
message << " With message: #{comment}" if comment
super(message)
end
end
end end

View File

@@ -1,72 +1,44 @@
require "socket"
require "websocket/driver"
require "digest/sha2"
require "json"
require "observer"
require "waitutil"
require_relative "mixin"
require_relative "error"
module OBSWS module OBSWS
class Socket class Identified
attr_reader :url attr_accessor :state
def initialize(url, socket) def initialize
@url = url @state = :pending
@socket = socket
end end
def write(s) def error_message
@socket.write(s) case @state
when :passwordless
"auth enabled but no password provided"
else
"failed to identify client with the websocket server"
end
end end
end end
class Base class Base
include Observable include Logging
include Driver::Director
include Mixin::OPCodes include Mixin::OPCodes
attr_reader :id, :driver, :closed attr_reader :closed, :identified
attr_writer :updater
def initialize(**kwargs) def initialize(**kwargs)
host = kwargs[:host] || "localhost" host = kwargs[:host] || "localhost"
port = kwargs[:port] || 4455 port = kwargs[:port] || 4455
@password = kwargs[:password] || "" @password = kwargs[:password] || ""
@subs = kwargs[:subs] || 0 @subs = kwargs[:subs] || 0
@identified = Identified.new
@socket = TCPSocket.new(host, port) setup_driver(host, port) and start_driver
@driver =
WebSocket::Driver.client(Socket.new("ws://#{host}:#{port}", @socket))
@driver.on :open do |msg|
LOGGER.debug("driver socket open")
end
@driver.on :close do |msg|
LOGGER.debug("driver socket closed")
@closed = true
end
@driver.on :message do |msg|
LOGGER.debug("received: #{msg.data}")
msg_handler(JSON.parse(msg.data, symbolize_names: true))
end
start_driver
WaitUtil.wait_for_condition( WaitUtil.wait_for_condition(
"successful identification", "successful identification",
delay_sec: 0.01, delay_sec: 0.01,
timeout_sec: 3 timeout_sec: kwargs[:connect_timeout] || 3
) { @identified } ) { @identified.state != :pending }
end end
def start_driver private
Thread.new do
@driver.start
loop do
@driver.parse(@socket.readpartial(4096))
rescue EOFError
break
end
end
end
def auth_token(salt:, challenge:) def auth_token(salt:, challenge:)
Digest::SHA256.base64digest( Digest::SHA256.base64digest(
@@ -84,9 +56,10 @@ module OBSWS
} }
if auth if auth
if @password.empty? if @password.empty?
raise OBSWSError("auth enabled but no password provided") @identified.state = :passwordless
return
end end
LOGGER.info("initiating authentication") logger.info("initiating authentication")
payload[:d][:authentication] = auth_token(**auth) payload[:d][:authentication] = auth_token(**auth)
end end
@driver.text(JSON.generate(payload)) @driver.text(JSON.generate(payload))
@@ -97,14 +70,13 @@ module OBSWS
when Mixin::OPCodes::HELLO when Mixin::OPCodes::HELLO
identify(data[:d][:authentication]) identify(data[:d][:authentication])
when Mixin::OPCodes::IDENTIFIED when Mixin::OPCodes::IDENTIFIED
@identified = true @identified.state = :identified
when Mixin::OPCodes::EVENT, Mixin::OPCodes::REQUESTRESPONSE when Mixin::OPCodes::EVENT, Mixin::OPCodes::REQUESTRESPONSE
changed @updater.call(data[:op], data[:d])
notify_observers(data[:op], data[:d])
end end
end end
def req(id, type_, data = nil) public def req(id, type_, data = nil)
payload = { payload = {
op: Mixin::OPCodes::REQUEST, op: Mixin::OPCodes::REQUEST,
d: { d: {
@@ -113,8 +85,8 @@ module OBSWS
} }
} }
payload[:d][:requestData] = data if data payload[:d][:requestData] = data if data
LOGGER.debug("sending request: #{payload}") logger.debug("sending request: #{payload}")
queued = @driver.text(JSON.generate(payload)) @driver.text(JSON.generate(payload))
end end
end end
end end

50
lib/obsws/driver.rb Normal file
View File

@@ -0,0 +1,50 @@
module OBSWS
module Driver
class Socket
attr_reader :url
def initialize(url, socket)
@url = url
@socket = socket
end
def write(s)
@socket.write(s)
end
end
module Director
def setup_driver(host, port)
@socket = TCPSocket.new(host, port)
@driver =
WebSocket::Driver.client(Socket.new("ws://#{host}:#{port}", @socket))
@driver.on :open do |msg|
logger.debug("driver socket open")
end
@driver.on :close do |msg|
logger.debug("driver socket closed")
@closed = true
end
@driver.on :message do |msg|
msg_handler(JSON.parse(msg.data, symbolize_names: true))
end
end
private def start_driver
Thread.new do
@driver.start
loop do
@driver.parse(@socket.readpartial(4096))
rescue EOFError
break
end
end
end
public def stop_driver
@driver.close
end
end
end
end

View File

@@ -1,6 +0,0 @@
module OBSWS
module Error
class OBSWSError < StandardError
end
end
end

View File

@@ -1,8 +1,3 @@
require "json"
require_relative "util"
require_relative "mixin"
module OBSWS module OBSWS
module Events module Events
module SUBS module SUBS
@@ -33,57 +28,65 @@ module OBSWS
ALL = LOW_VOLUME | HIGH_VOLUME ALL = LOW_VOLUME | HIGH_VOLUME
end end
module Callbacks module EventDirector
include Util include Util::String
def add_observer(observer) def observers
@observers = [] unless defined?(@observers) @observers ||= {}
observer = [observer] if !observer.respond_to? :each
observer.each { |o| @observers.append(o) }
end end
def remove_observer(observer) def on(event, method = nil, &block)
@observers.delete(observer) (observers[event] ||= []) << (block || method)
end end
def notify_observers(event, data) def register(cbs)
if defined?(@observers) cbs = [cbs] unless cbs.respond_to? :each
@observers.each do |o| cbs.each { |cb| on(cb.name[3..].to_sym, cb) }
if o.respond_to? "on_#{event.to_snake}"
if data.empty?
o.send("on_#{event.to_snake}")
else
o.send("on_#{event.to_snake}", data)
end
end
end end
def deregister(cbs)
cbs = [cbs] unless cbs.respond_to? :each
cbs.each { |cb| observers[cb.name[3..].to_sym]&.reject! { |o| cbs.include? o } }
end end
def fire(event, data)
observers[snakecase(event).to_sym]&.each { |block| data.empty? ? block.call : block.call(data) }
end end
end end
class Client class Client
include Callbacks include Logging
include EventDirector
include Mixin::TearDown include Mixin::TearDown
include Mixin::OPCodes include Mixin::OPCodes
def initialize(**kwargs) def initialize(**kwargs)
kwargs[:subs] ||= SUBS::LOW_VOLUME kwargs[:subs] ||= SUBS::LOW_VOLUME
@base_client = Base.new(**kwargs) @base_client = Base.new(**kwargs)
LOGGER.info("#{self} succesfully identified with server") unless @base_client.identified.state == :identified
@base_client.add_observer(self) err_msg = @base_client.identified.error_message
logger.error(err_msg)
raise OBSWSConnectionError.new(err_msg)
end
logger.info("#{self} successfully identified with server")
rescue Errno::ECONNREFUSED, WaitUtil::TimeoutError => e
msg = "#{e.class.name}: #{e.message}"
logger.error(msg)
raise OBSWSConnectionError.new(msg)
else
@base_client.updater = ->(op_code, data) {
if op_code == Mixin::OPCodes::EVENT
logger.debug("received: #{data}")
event = data[:eventType]
data = data.fetch(:eventData, {})
fire(event, Mixin::Data.new(data, data.keys))
end
}
end end
def to_s def to_s
self.class.name.split("::").last(2).join("::") self.class.name.split("::").last(2).join("::")
end end
def update(op_code, data)
if op_code == Mixin::OPCodes::EVENT
event = data[:eventType]
data = data.key?(:eventData) ? data[:eventData] : {}
notify_observers(event, Mixin::Data.new(data, data.keys))
end
end
end end
end end
end end

9
lib/obsws/logger.rb Normal file
View File

@@ -0,0 +1,9 @@
module OBSWS
module Logging
def logger
@logger ||= Logger.new($stdout, level: ENV.fetch("OBSWS_LOG_LEVEL", "WARN"))
@logger.progname = instance_of?(::Module) ? name : self.class.name
@logger
end
end
end

View File

@@ -1,13 +1,11 @@
require_relative "util"
module OBSWS module OBSWS
module Mixin module Mixin
module Meta module Meta
include Util include Util::String
def make_field_methods(*params) def make_field_methods(*params)
params.each do |param| params.each do |param|
define_singleton_method(param.to_s.to_snake) { @resp[param] } define_singleton_method(snakecase(param.to_s)) { @resp[param] }
end end
end end
end end
@@ -18,24 +16,26 @@ module OBSWS
def initialize(resp, fields) def initialize(resp, fields)
@resp = resp @resp = resp
@fields = fields @fields = fields
self.make_field_methods *fields make_field_methods(*fields)
end end
def empty? = @fields.empty? def empty? = @fields.empty?
def attrs = @fields.map { |f| f.to_s.to_snake } def attrs = @fields.map { |f| snakecase(f.to_s) }
end end
class Response < MetaObject class Response < MetaObject; end
end # Represents a request response object
class Data < MetaObject class Data < MetaObject; end
end # Represents an event data object
module TearDown module TearDown
def close def stop_driver
@base_client.driver.close @base_client.stop_driver
end end
alias_method :close, :stop_driver
end end
module OPCodes module OPCodes

View File

@@ -1,21 +1,26 @@
require "waitutil"
require_relative "base"
require_relative "error"
require_relative "util"
require_relative "mixin"
module OBSWS module OBSWS
module Requests module Requests
class Client class Client
include Error include Logging
include Mixin::TearDown include Mixin::TearDown
include Mixin::OPCodes include Mixin::OPCodes
def initialize(**kwargs) def initialize(**kwargs)
@base_client = Base.new(**kwargs) @base_client = Base.new(**kwargs)
LOGGER.info("#{self} succesfully identified with server") unless @base_client.identified.state == :identified
@base_client.add_observer(self) err_msg = @base_client.identified.error_message
logger.error(err_msg)
raise OBSWSConnectionError.new(err_msg)
end
logger.info("#{self} successfully identified with server")
rescue Errno::ECONNREFUSED, WaitUtil::TimeoutError => e
logger.error("#{e.class.name}: #{e.message}")
raise OBSWSConnectionError.new(e.message)
else
@base_client.updater = ->(op_code, data) {
logger.debug("response received: #{data}")
@response = data if op_code == Mixin::OPCodes::REQUESTRESPONSE
}
@response = {requestId: 0} @response = {requestId: 0}
end end
@@ -26,7 +31,7 @@ module OBSWS
def run def run
yield(self) yield(self)
ensure ensure
close stop_driver
WaitUtil.wait_for_condition( WaitUtil.wait_for_condition(
"driver to close", "driver to close",
delay_sec: 0.01, delay_sec: 0.01,
@@ -34,32 +39,24 @@ module OBSWS
) { @base_client.closed } ) { @base_client.closed }
end end
def update(op_code, data)
@response = data if op_code == Mixin::OPCodes::REQUESTRESPONSE
end
def call(req, data = nil) def call(req, data = nil)
id = rand(1..1000) uuid = SecureRandom.uuid
@base_client.req(id, req, data) @base_client.req(uuid, req, data)
WaitUtil.wait_for_condition( WaitUtil.wait_for_condition(
"reponse id to match request id", "reponse id to match request id",
delay_sec: 0.001, delay_sec: 0.001,
timeout_sec: 3 timeout_sec: 3
) { @response[:requestId] == id } ) { @response[:requestId] == uuid }
if !@response[:requestStatus][:result] unless @response[:requestStatus][:result]
error = [ raise OBSWSRequestError.new(@response[:requestType], @response[:requestStatus][:code], @response[:requestStatus][:comment])
"Request #{@response[:requestType]} returned code #{@response[:requestStatus][:code]}"
]
if @response[:requestStatus].key?(:comment)
error += ["With message: #{@response[:requestStatus][:comment]}"]
end
raise OBSWSError.new(error.join("\n"))
end end
@response[:responseData] @response[:responseData]
rescue WaitUtil::TimeoutError rescue OBSWSRequestError => e
msg = "no response with matching id received" logger.error(["#{e.class.name}: #{e.message}", *e.backtrace].join("\n"))
LOGGER.error(msg) raise
raise OBSWSError.new(msg) rescue WaitUtil::TimeoutError => e
logger.error(["#{e.class.name}: #{e.message}", *e.backtrace].join("\n"))
raise OBSWSError.new([e.message, *e.backtrace].join("\n"))
end end
def get_version def get_version

View File

@@ -1,12 +1,12 @@
module OBSWS module OBSWS
module Util module Util
class ::String module String
def to_camel def camelcase(s)
self.split(/_/).map(&:capitalize).join s.split("_").map(&:capitalize).join
end end
def to_snake def snakecase(s)
self s
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2') .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
.gsub(/([a-z\d])([A-Z])/, '\1_\2') .gsub(/([a-z\d])([A-Z])/, '\1_\2')
.downcase .downcase

View File

@@ -1,5 +1,5 @@
module OBSWS module OBSWS
module Version module VERSION
module_function module_function
def major def major
@@ -7,7 +7,7 @@ module OBSWS
end end
def minor def minor
2 6
end end
def patch def patch
@@ -22,6 +22,4 @@ module OBSWS
to_a.join(".") to_a.join(".")
end end
end end
VERSION = Version.to_s
end end

24
main.rb
View File

@@ -1,14 +1,18 @@
require_relative "lib/obsws" require "obsws"
def main class Main
OBSWS::Requests::Client.new( INPUT = "Mic/Aux"
host: "localhost",
port: 4455, def run
password: "strongpassword" OBSWS::Requests::Client
).run do |client| .new(host: "localhost", port: 4455, password: "strongpassword")
# Toggle the mute state of your Mic input .run do |client|
client.toggle_input_mute("Mic/Aux") # Toggle the mute state of your Mic input and print its new mute state
client.toggle_input_mute(INPUT)
resp = client.get_input_mute(INPUT)
puts "Input '#{INPUT}' was set to #{resp.input_muted}"
end
end end
end end
main if $0 == __FILE__ Main.new.run if $PROGRAM_NAME == __FILE__

View File

@@ -13,7 +13,6 @@ Gem::Specification.new do |spec|
spec.extra_rdoc_files = Dir["README.md", "CHANGELOG.md", "LICENSE"] spec.extra_rdoc_files = Dir["README.md", "CHANGELOG.md", "LICENSE"]
spec.homepage = "https://rubygems.org/gems/obsws" spec.homepage = "https://rubygems.org/gems/obsws"
spec.license = "MIT" spec.license = "MIT"
spec.add_runtime_dependency "observer", "~> 0.1.1"
spec.add_runtime_dependency "websocket-driver", "~> 0.7.5" spec.add_runtime_dependency "websocket-driver", "~> 0.7.5"
spec.add_runtime_dependency "waitutil", "~> 0.2.1" spec.add_runtime_dependency "waitutil", "~> 0.2.1"
spec.add_development_dependency "standard", "~> 1.30" spec.add_development_dependency "standard", "~> 1.30"

View File

@@ -1,6 +1,6 @@
require_relative "../minitest_helper" require_relative "../minitest_helper"
class AttrsTest < OBSWSTest class AttrsTest < Minitest::Test
def test_get_version_attrs def test_get_version_attrs
resp = OBSWSTest.r_client.get_version resp = OBSWSTest.r_client.get_version
assert resp.attrs == assert resp.attrs ==

36
test/obsws/test_error.rb Normal file
View File

@@ -0,0 +1,36 @@
require_relative "../minitest_helper"
class OBSWSConnectionErrorTest < Minitest::Test
def test_it_raises_an_obsws_connection_error_on_wrong_password
e = assert_raises(OBSWS::OBSWSConnectionError) do
OBSWS::Requests::Client
.new(host: "localhost", port: 4455, password: "wrongpassword", connect_timeout: 0.1)
end
assert_equal("Timed out waiting for successful identification (0.1 seconds elapsed)", e.message)
end
def test_it_raises_an_obsws_connection_error_on_auth_enabled_but_no_password_provided_for_request_client
e = assert_raises(OBSWS::OBSWSConnectionError) do
OBSWS::Requests::Client
.new(host: "localhost", port: 4455, password: "")
end
assert_equal("auth enabled but no password provided", e.message)
end
def test_it_raises_an_obsws_connection_error_on_auth_enabled_but_no_password_provided_for_event_client
e = assert_raises(OBSWS::OBSWSConnectionError) do
OBSWS::Events::Client
.new(host: "localhost", port: 4455, password: "")
end
assert_equal("auth enabled but no password provided", e.message)
end
end
class OBSWSRequestErrorTest < Minitest::Test
def test_it_raises_an_obsws_request_error_on_invalid_request
e = assert_raises(OBSWS::OBSWSRequestError) { OBSWSTest.r_client.toggle_input_mute("unknown") }
assert_equal("ToggleInputMute", e.req_name)
assert_equal(600, e.code)
assert_equal("Request ToggleInputMute returned code 600. With message: No source was found by the name of `unknown`.", e.message)
end
end

View File

@@ -1,6 +1,6 @@
require_relative "../minitest_helper" require_relative "../minitest_helper"
class RequestTest < OBSWSTest class RequestTest < Minitest::Test
def test_it_checks_obs_major_version def test_it_checks_obs_major_version
resp = OBSWSTest.r_client.get_version resp = OBSWSTest.r_client.get_version
ver = resp.obs_version.split(".").map(&:to_i) ver = resp.obs_version.split(".").map(&:to_i)