76 Commits

Author SHA1 Message Date
3c892623fa Merge pull request #6 from onyx-and-iris/dependabot/bundler/rexml-3.4.2
Bump rexml from 3.3.9 to 3.4.2
2026-03-17 21:16:57 +00:00
dependabot[bot]
92b36ecce4 Bump rexml from 3.3.9 to 3.4.2
Bumps [rexml](https://github.com/ruby/rexml) from 3.3.9 to 3.4.2.
- [Release notes](https://github.com/ruby/rexml/releases)
- [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md)
- [Commits](https://github.com/ruby/rexml/compare/v3.3.9...v3.4.2)

---
updated-dependencies:
- dependency-name: rexml
  dependency-version: 3.4.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-17 21:15:05 +00:00
1dc8bf10f7 Merge pull request #5 from onyx-and-iris/dependabot/bundler/rexml-3.3.9
Bump rexml from 3.3.6 to 3.3.9
2025-03-27 15:46:02 +00:00
dependabot[bot]
426a7d6628 Bump rexml from 3.3.6 to 3.3.9
Bumps [rexml](https://github.com/ruby/rexml) from 3.3.6 to 3.3.9.
- [Release notes](https://github.com/ruby/rexml/releases)
- [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md)
- [Commits](https://github.com/ruby/rexml/compare/v3.3.6...v3.3.9)

---
updated-dependencies:
- dependency-name: rexml
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-27 15:45:47 +00:00
e1146913f9 Merge pull request #4 from onyx-and-iris/dependabot/bundler/rexml-3.3.6
Bump rexml from 3.3.3 to 3.3.6
2024-08-24 14:37:10 +01:00
dependabot[bot]
78ca03e610 Bump rexml from 3.3.3 to 3.3.6
Bumps [rexml](https://github.com/ruby/rexml) from 3.3.3 to 3.3.6.
- [Release notes](https://github.com/ruby/rexml/releases)
- [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md)
- [Commits](https://github.com/ruby/rexml/compare/v3.3.3...v3.3.6)

---
updated-dependencies:
- dependency-name: rexml
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-24 13:37:01 +00:00
03174b9e40 Merge pull request #3 from onyx-and-iris/dependabot/bundler/rexml-3.3.3
Bump rexml from 3.3.0 to 3.3.3
2024-08-06 11:37:52 +01:00
dependabot[bot]
63dd131d61 Bump rexml from 3.3.0 to 3.3.3
Bumps [rexml](https://github.com/ruby/rexml) from 3.3.0 to 3.3.3.
- [Release notes](https://github.com/ruby/rexml/releases)
- [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md)
- [Commits](https://github.com/ruby/rexml/compare/v3.3.0...v3.3.3)

---
updated-dependencies:
- dependency-name: rexml
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-06 10:37:37 +00:00
6ffae8e6c7 Merge pull request #2 from onyx-and-iris/dependabot/bundler/rexml-3.3.0
Bump rexml from 3.2.5 to 3.3.0
2024-06-21 08:32:05 +01:00
dependabot[bot]
2878c31819 Bump rexml from 3.2.5 to 3.3.0
Bumps [rexml](https://github.com/ruby/rexml) from 3.2.5 to 3.3.0.
- [Release notes](https://github.com/ruby/rexml/releases)
- [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md)
- [Commits](https://github.com/ruby/rexml/compare/v3.2.5...v3.3.0)

---
updated-dependencies:
- dependency-name: rexml
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-21 07:31:42 +00:00
2e774ef185 Merge pull request #1 from onyx-and-iris/dependabot/bundler/rake-13.0.6
Update rake requirement from ~> 11.2, >= 11.2.2 to >= 11.2.2, ~> 13.0
2023-09-03 16:41:03 +01:00
dependabot[bot]
3e661b23a3 Update rake requirement from ~> 11.2, >= 11.2.2 to >= 11.2.2, ~> 13.0
Updates the requirements on [rake](https://github.com/ruby/rake) to permit the latest version.
- [Release notes](https://github.com/ruby/rake/releases)
- [Changelog](https://github.com/ruby/rake/blob/master/History.rdoc)
- [Commits](https://github.com/ruby/rake/compare/v11.3.0...v13.0.6)

---
updated-dependencies:
- dependency-name: rake
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-03 15:40:27 +00:00
fac83a0dde upd Gemfile.lock 2023-08-30 22:35:02 +01:00
4c4746fe8a now using String refinement
Events Director module renamed

patch bump
2023-08-29 22:55:11 +01:00
3a0f298045 upd gemfile 2023-08-29 20:36:51 +01:00
a4b70b6a98 patch bump 2023-08-29 15:46:00 +01:00
7c20063866 add error class docstrings 2023-08-29 15:45:42 +01:00
7cf05fbe08 use Kernel#Array 2023-08-29 15:45:23 +01:00
587e861bbe upd Gemfile.lock 2023-08-18 21:51:22 +01:00
3f255d6b45 upd attribute ssection in readme 2023-08-17 23:31:26 +01:00
9bd2b53b3d udpate attributes section in README 2023-08-17 23:28:37 +01:00
61c9a7b365 reword in readme.
remove brackets
2023-08-17 23:26:34 +01:00
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 405 additions and 409 deletions

View File

@@ -1,8 +1,7 @@
PATH
remote: .
specs:
obsws (0.2.0)
observer (~> 0.1.1)
obsws (0.6.2)
waitutil (~> 0.2.1)
websocket-driver (~> 0.7.5)
@@ -14,16 +13,15 @@ GEM
language_server-protocol (3.17.0.3)
lint_roller (1.1.0)
minitest (5.16.3)
observer (0.1.1)
parallel (1.23.0)
parser (3.2.2.3)
ast (~> 2.4.1)
racc
racc (1.7.1)
rainbow (3.1.1)
rake (11.3.0)
rake (13.0.6)
regexp_parser (2.8.1)
rexml (3.2.5)
rexml (3.4.2)
rubocop (1.52.1)
json (~> 2.3)
parallel (~> 1.10)
@@ -54,17 +52,18 @@ GEM
rubocop-performance (~> 1.18.0)
unicode-display_width (2.4.2)
waitutil (0.2.1)
websocket-driver (0.7.5)
websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
PLATFORMS
x64-mingw-ucrt
x86_64-linux
DEPENDENCIES
minitest (~> 5.16, >= 5.16.3)
obsws!
rake (~> 11.2, >= 11.2.2)
rake (~> 13.0, >= 11.2.2)
standard (~> 1.30)
BUNDLED WITH

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)
[![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
@@ -16,75 +16,75 @@
### Bundler
```
bundle add 'obsws'
bundle add obsws
bundle install
```
### Gem
`gem install 'obsws'`
## `Use`
#### Example `main.rb`
pass `host`, `port` and `password` as keyword arguments.
Pass `host`, `port` and `password` as keyword arguments.
```ruby
require "obsws"
def main
class Main
INPUT = "Mic/Aux"
def run
OBSWS::Requests::Client
.new(host: "localhost", port: 4455, password: "strongpassword")
.run do |client|
# Toggle the mute state of your Mic input
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
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
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:
```ruby
r_client.run do
# GetVersion
resp = r_client.get_version
# SetCurrentProgramScene
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)
### 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. Event tokens should match the event name but snake cased.
The event data will be passed to the block.
example:
```ruby
class Observer
def initialize
@e_client = OBSWS::Events::Client.new(**kwargs)
# register class with the event client
@e_client.add_observer(self)
end
# define "on_" event methods.
def on_current_program_scene_changed
@e_client = OBSWS::Events::Client.new(host: "localhost", port: 4455, password: "strongpassword")
# register blocks on event types.
@e_client.on :current_program_scene_changed do |data|
...
end
def on_input_mute_state_changed
@e_client.on :input_mute_state_changed do |data|
...
end
...
end
end
```
@@ -97,30 +97,36 @@ For both request responses and event data you may inspect the available attribut
example:
```ruby
resp = cl.get_version
resp = @r_client.get_version
p resp.attrs
def on_scene_created(data):
@e_client.on :input_mute_state_changed do |data|
p data.attrs
end
```
### 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)
### 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
require "obsws"
OBSWS::LOGGER.debug!
...
```powershell
$env:OBSWS_LOG_LEVEL="DEBUG"
```
### Tests

View File

@@ -5,19 +5,25 @@ HERE = __dir__
Minitest::TestTask.create(:test) do |t|
t.libs << "test"
t.warning = false
t.test_globs = ["test/**/*_test.rb"]
t.test_globs = ["test/**/test_*.rb"]
end
task default: :test
namespace :e do
desc "Runs the events example"
task :events do
filepath = File.join(HERE, "examples", "events", "main.rb")
ruby filepath
end
desc "Runs the levels example"
task :levels do
filepath = File.join(HERE, "examples", "levels", "main.rb")
ruby filepath
end
desc "Runs the scene_rotate example"
task :scene_rotate do
filepath = File.join(HERE, "examples", "scene_rotate", "main.rb")
ruby filepath
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"
OBSWS::LOGGER.info!
class Main
attr_reader :running
def initialize(**kwargs)
@r_client = OBSWS::Requests::Client.new(**kwargs)
@e_client = OBSWS::Events::Client.new(**kwargs)
@e_client.add_observer(self)
puts info.join("\n")
@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)
@e_client.on :current_program_scene_changed do |data|
puts "Switched to scene #{data.scene_name}"
end
def on_scene_created(data)
@e_client.on :scene_created do |data|
puts "scene #{data.scene_name} has been created"
end
def on_input_mute_state_changed(data)
@e_client.on :input_mute_state_changed do |data|
puts "#{data.input_name} mute toggled"
end
def on_exit_started
@e_client.on :exit_started do
puts "OBS closing!"
@r_client.close
@e_client.close
@running = false
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
def conn_from_yaml
YAML.load_file("obs.yml", symbolize_names: true)[:connection]
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"
OBSWS::LOGGER.info!
DEVICE = "Desktop Audio"
module LevelTypes
VU = 0
POSTFADER = 1
@@ -11,24 +8,18 @@ module LevelTypes
end
class Main
DEVICE = "Desktop Audio"
def initialize(**kwargs)
subs = OBSWS::Events::SUBS::LOW_VOLUME | OBSWS::Events::SUBS::INPUTVOLUMEMETERS
@e_client = OBSWS::Events::Client.new(subs:, **kwargs)
@e_client.add_observer(self)
end
def run
puts "press <Enter> to quit"
exit if gets.chomp.empty?
end
def on_input_mute_state_changed(data)
@e_client.on :input_mute_state_changed do |data|
if data.input_name == DEVICE
puts "#{DEVICE} mute toggled"
end
end
def on_input_volume_meters(data)
@e_client.on :input_volume_meters do |data|
fget = ->(x) { (x > 0) ? (20 * Math.log(x, 10)).round(1) : -200.0 }
data.inputs.each do |d|
@@ -41,8 +32,14 @@ class Main
end
end
def run
puts "press <Enter> to quit"
loop { break if gets.chomp.empty? }
end
end
def conn_from_yaml
YAML.load_file("obs.yml", symbolize_names: true)[:connection]
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"
OBSWS::LOGGER.info!
class Main
def conn_from_yaml
YAML.load_file("obs.yml", symbolize_names: true)[:connection]
end
def main
def run
OBSWS::Requests::Client.new(**conn_from_yaml).run do |client|
resp = client.get_scene_list
resp.scenes.reverse_each do |scene|
@@ -17,5 +17,7 @@ def main
end
end
end
end
main if $0 == __FILE__
Main.new.run if $PROGRAM_NAME == __FILE__

View File

@@ -1,11 +1,39 @@
require "digest/sha2"
require "json"
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/event"
module OBSWS
include Logger::Severity
require_relative "obsws/version"
LOGGER = Logger.new(STDOUT)
LOGGER.level = WARN
module OBSWS
# Base OBSWS error class
class OBSWSError < StandardError; end
# Raised when a connection fails or times out
class OBSWSConnectionError < OBSWSError; end
# Raised when a request returns an error code
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

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
class Socket
attr_reader :url
class Identified
attr_accessor :state
def initialize(url, socket)
@url = url
@socket = socket
def initialize
@state = :pending
end
def write(s)
@socket.write(s)
def error_message
case @state
when :passwordless
"auth enabled but no password provided"
else
"failed to identify client with the websocket server"
end
end
end
class Base
include Observable
include Logging
include Driver::Director
include Mixin::OPCodes
attr_reader :id, :driver, :closed
attr_reader :closed, :identified
attr_writer :updater
def initialize(**kwargs)
host = kwargs[:host] || "localhost"
port = kwargs[:port] || 4455
@password = kwargs[:password] || ""
@subs = kwargs[:subs] || 0
@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|
LOGGER.debug("received: #{msg.data}")
msg_handler(JSON.parse(msg.data, symbolize_names: true))
end
start_driver
@identified = Identified.new
setup_driver(host, port) and start_driver
WaitUtil.wait_for_condition(
"successful identification",
delay_sec: 0.01,
timeout_sec: 3
) { @identified }
timeout_sec: kwargs[:connect_timeout] || 3
) { @identified.state != :pending }
end
def start_driver
Thread.new do
@driver.start
loop do
@driver.parse(@socket.readpartial(4096))
rescue EOFError
break
end
end
end
private
def auth_token(salt:, challenge:)
Digest::SHA256.base64digest(
@@ -84,9 +56,10 @@ module OBSWS
}
if auth
if @password.empty?
raise OBSWSError("auth enabled but no password provided")
@identified.state = :passwordless
return
end
LOGGER.info("initiating authentication")
logger.info("initiating authentication")
payload[:d][:authentication] = auth_token(**auth)
end
@driver.text(JSON.generate(payload))
@@ -97,14 +70,13 @@ module OBSWS
when Mixin::OPCodes::HELLO
identify(data[:d][:authentication])
when Mixin::OPCodes::IDENTIFIED
@identified = true
@identified.state = :identified
when Mixin::OPCodes::EVENT, Mixin::OPCodes::REQUESTRESPONSE
changed
notify_observers(data[:op], data[:d])
@updater.call(data[:op], data[:d])
end
end
def req(id, type_, data = nil)
public def req(id, type_, data = nil)
payload = {
op: Mixin::OPCodes::REQUEST,
d: {
@@ -113,8 +85,8 @@ module OBSWS
}
}
payload[:d][:requestData] = data if data
LOGGER.debug("sending request: #{payload}")
queued = @driver.text(JSON.generate(payload))
logger.debug("sending request: #{payload}")
@driver.text(JSON.generate(payload))
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 Events
module SUBS
@@ -33,57 +28,65 @@ module OBSWS
ALL = LOW_VOLUME | HIGH_VOLUME
end
module Callbacks
include Util
module Director
using Util::CoreExtensions
def add_observer(observer)
@observers = [] unless defined?(@observers)
observer = [observer] if !observer.respond_to? :each
observer.each { |o| @observers.append(o) }
def observers
@observers ||= {}
end
def remove_observer(observer)
@observers.delete(observer)
def on(event, method = nil, &block)
(observers[event] ||= []) << (block || method)
end
def notify_observers(event, data)
if defined?(@observers)
@observers.each do |o|
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
def register(cbs)
cbs = Array(cbs) unless cbs.respond_to? :each
cbs.each { |cb| on(cb.name[3..].to_sym, cb) }
end
def deregister(cbs)
cbs = Array(cbs) unless cbs.respond_to? :each
cbs.each { |cb| observers[cb.name[3..].to_sym]&.reject! { |o| cbs.include? o } }
end
def fire(event, data)
observers[event.snakecase.to_sym]&.each { |block| data.empty? ? block.call : block.call(data) }
end
end
class Client
include Callbacks
include Logging
include Events::Director
include Mixin::TearDown
include Mixin::OPCodes
def initialize(**kwargs)
kwargs[:subs] ||= SUBS::LOW_VOLUME
@base_client = Base.new(**kwargs)
LOGGER.info("#{self} succesfully identified with server")
@base_client.add_observer(self)
unless @base_client.identified.state == :identified
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
def to_s
self.class.name.split("::").last(2).join("::")
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

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,41 +1,42 @@
require_relative "util"
module OBSWS
module Mixin
module Meta
include Util
using Util::CoreExtensions
def make_field_methods(*params)
params.each do |param|
define_singleton_method(param.to_s.to_snake) { @resp[param] }
define_singleton_method(param.to_s.snakecase) { @resp[param] }
end
end
end
class MetaObject
using Util::CoreExtensions
include Mixin::Meta
def initialize(resp, fields)
@resp = resp
@fields = fields
self.make_field_methods *fields
make_field_methods(*fields)
end
def empty? = @fields.empty?
def attrs = @fields.map { |f| f.to_s.to_snake }
def attrs = @fields.map { |f| f.to_s.snakecase }
end
class Response < MetaObject
end
# Represents a request response object
class Response < MetaObject; end
class Data < MetaObject
end
# Represents an event data object
class Data < MetaObject; end
module TearDown
def close
@base_client.driver.close
def stop_driver
@base_client.stop_driver
end
alias_method :close, :stop_driver
end
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 Requests
class Client
include Error
include Logging
include Mixin::TearDown
include Mixin::OPCodes
def initialize(**kwargs)
@base_client = Base.new(**kwargs)
LOGGER.info("#{self} succesfully identified with server")
@base_client.add_observer(self)
unless @base_client.identified.state == :identified
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}
end
@@ -26,7 +31,7 @@ module OBSWS
def run
yield(self)
ensure
close
stop_driver
WaitUtil.wait_for_condition(
"driver to close",
delay_sec: 0.01,
@@ -34,32 +39,24 @@ module OBSWS
) { @base_client.closed }
end
def update(op_code, data)
@response = data if op_code == Mixin::OPCodes::REQUESTRESPONSE
end
def call(req, data = nil)
id = rand(1..1000)
@base_client.req(id, req, data)
uuid = SecureRandom.uuid
@base_client.req(uuid, req, data)
WaitUtil.wait_for_condition(
"reponse id to match request id",
delay_sec: 0.001,
timeout_sec: 3
) { @response[:requestId] == id }
if !@response[:requestStatus][:result]
error = [
"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"))
) { @response[:requestId] == uuid }
unless @response[:requestStatus][:result]
raise OBSWSRequestError.new(@response[:requestType], @response[:requestStatus][:code], @response[:requestStatus][:comment])
end
@response[:responseData]
rescue WaitUtil::TimeoutError
msg = "no response with matching id received"
LOGGER.error(msg)
raise OBSWSError.new(msg)
rescue OBSWSRequestError => e
logger.error(["#{e.class.name}: #{e.message}", *e.backtrace].join("\n"))
raise
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
def get_version

View File

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

View File

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

24
main.rb
View File

@@ -1,14 +1,18 @@
require_relative "lib/obsws"
require "obsws"
def main
OBSWS::Requests::Client.new(
host: "localhost",
port: 4455,
password: "strongpassword"
).run do |client|
# Toggle the mute state of your Mic input
client.toggle_input_mute("Mic/Aux")
class Main
INPUT = "Mic/Aux"
def run
OBSWS::Requests::Client
.new(host: "localhost", port: 4455, password: "strongpassword")
.run do |client|
# 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
main if $0 == __FILE__
Main.new.run if $PROGRAM_NAME == __FILE__

View File

@@ -13,12 +13,11 @@ Gem::Specification.new do |spec|
spec.extra_rdoc_files = Dir["README.md", "CHANGELOG.md", "LICENSE"]
spec.homepage = "https://rubygems.org/gems/obsws"
spec.license = "MIT"
spec.add_runtime_dependency "observer", "~> 0.1.1"
spec.add_runtime_dependency "websocket-driver", "~> 0.7.5"
spec.add_runtime_dependency "waitutil", "~> 0.2.1"
spec.add_development_dependency "standard", "~> 1.30"
spec.add_development_dependency "minitest", "~> 5.16", ">= 5.16.3"
spec.add_development_dependency "rake", "~> 11.2", ">= 11.2.2"
spec.add_development_dependency "rake", ">= 11.2.2", "~> 13.0"
spec.required_ruby_version = ">= 3.0"
spec.metadata = {
"source_code_uri" => "https://github.com/onyx-and-iris/obsws-ruby"

View File

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