Compare commits

60 Commits

Author SHA1 Message Date
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
856b7b5a5b add attr_reader r_client for OBSWSTest 2023-07-19 15:24:15 +01:00
af85d8b9ab minor version bump 2023-07-19 15:14:46 +01:00
5f08b97759 example in readme and main.rb refactored
code style badge changed to standard
2023-07-19 15:14:28 +01:00
73ae24eb4b examples refactored 2023-07-19 15:13:26 +01:00
153f35e742 now using symbols when making api calls
in run, yield now passes a reference ot the client as an argument
2023-07-19 15:12:25 +01:00
17dcaa18d8 now using standard 2023-07-19 15:11:15 +01:00
955b00571a whitespace fix 2023-07-19 15:11:05 +01:00
d9b054b108 whitespace fix
load obs.yml instead
2023-07-19 15:10:38 +01:00
799ae52b02 add prettier settings 2023-05-26 21:17:17 +01:00
bc93654297 move identify payload out of auth block
patch bump
2022-11-25 18:05:10 +00:00
aafcd185d0 patch bump
add full request payload to logger.

raise OBSWSError if auth enabled but no password provided

add example tasks to rake file

add logging section to README
2022-11-25 17:57:56 +00:00
43ecfb37f5 upd gemfile.lock 2022-11-18 15:00:34 +00:00
2e70c63ae7 fix patch ver 2022-11-18 14:54:29 +00:00
c67ce47026 pass payload for call_vendor_request
patch bump
2022-11-18 14:52:28 +00:00
norm
0bac7eaf3a added missing response classes
patch bump
2022-11-03 04:11:59 +00:00
norm
5b0ce79e46 fix bug in get_group_list
patch bump
2022-11-03 03:36:56 +00:00
norm
a0f5d8e57b add levels example 2022-10-27 06:45:52 +01:00
norm
f1a1c970e0 add low, high, all constants to SUBS 2022-10-27 06:45:21 +01:00
onyx-and-iris
449684c405 upd gemfile.lock files 2022-10-25 00:33:24 +01:00
onyx-and-iris
da5ef76c81 minor bump 2022-10-25 00:06:10 +01:00
onyx-and-iris
8752132012 scene rotate example added 2022-10-25 00:05:12 +01:00
onyx-and-iris
fb162ca195 logger level set to info in events example 2022-10-25 00:04:28 +01:00
onyx-and-iris
b33fe94cee moved logger auth success to req,event classes
override to_s for req,event classes

rename authenticate to identify in base class
2022-10-25 00:03:43 +01:00
26 changed files with 717 additions and 545 deletions

7
.gitignore vendored
View File

@@ -46,7 +46,8 @@ build-iPhoneSimulator/
# for a library or gem, you might want to ignore these files since the code is # for a library or gem, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in: # intended to run in multiple environments; otherwise, check them in:
# Gemfile.lock # Gemfile.lock
# .ruby-version .ruby-version
.ruby_version
# .ruby-gemset # .ruby-gemset
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this: # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
@@ -56,6 +57,8 @@ build-iPhoneSimulator/
# .rubocop-https?--* # .rubocop-https?--*
# config # config
obs.toml obs.yml
# quick test # quick test
quick.rb quick.rb
.vscode/

View File

@@ -1,20 +1,58 @@
PATH PATH
remote: . remote: .
specs: specs:
obsws (0.0.3) obsws (0.5.3)
observer (~> 0.1.1)
waitutil (~> 0.2.1) waitutil (~> 0.2.1)
websocket-driver (~> 0.7.5) websocket-driver (~> 0.7.5)
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
ast (2.4.2)
json (2.6.3)
language_server-protocol (3.17.0.3)
lint_roller (1.1.0)
minitest (5.16.3) minitest (5.16.3)
observer (0.1.1) parallel (1.23.0)
perfect_toml (0.9.0) parser (3.2.2.3)
ast (~> 2.4.1)
racc
racc (1.7.1)
rainbow (3.1.1)
rake (11.3.0) rake (11.3.0)
regexp_parser (2.8.1)
rexml (3.2.5)
rubocop (1.52.1)
json (~> 2.3)
parallel (~> 1.10)
parser (>= 3.2.2.3)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.28.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.29.0)
parser (>= 3.2.1.0)
rubocop-performance (1.18.0)
rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0)
ruby-progressbar (1.13.0)
standard (1.30.1)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.0)
rubocop (~> 1.52.0)
standard-custom (~> 1.0.0)
standard-performance (~> 1.1.0)
standard-custom (1.0.2)
lint_roller (~> 1.0)
rubocop (~> 1.50)
standard-performance (1.1.2)
lint_roller (~> 1.1)
rubocop-performance (~> 1.18.0)
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)
@@ -24,8 +62,8 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
minitest (~> 5.16, >= 5.16.3) minitest (~> 5.16, >= 5.16.3)
obsws! obsws!
perfect_toml (~> 0.9.0)
rake (~> 11.2, >= 11.2.2) rake (~> 11.2, >= 11.2.2)
standard (~> 1.30)
BUNDLED WITH BUNDLED WITH
2.3.22 2.3.22

View File

@@ -1,8 +1,8 @@
[![Gem Version](https://badge.fury.io/rb/obsws.svg)](https://badge.fury.io/rb/obsws) [![Gem Version](https://badge.fury.io/rb/obsws.svg)](https://badge.fury.io/rb/obsws)
[![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)
[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/plugin-ruby) [![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,77 +16,75 @@
### 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
r_client = def run
OBSWS::Requests::Client.new( OBSWS::Requests::Client
host: "localhost", .new(host: "localhost", port: 4455, password: "strongpassword")
port: 4455, .run do |client|
password: "strongpassword"
)
r_client.run do
# Toggle the mute state of your Mic input # Toggle the mute state of your Mic input
r_client.toggle_input_mute("Mic/Aux") client.toggle_input_mute("Mic/Aux")
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 `on_` callback methods. Method names should match the api event but snake cased.
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 callback methods with the Event client
@e_client.add_observer(self) @e_client.register(
[
method(:on_current_program_scene_changed),
method(:on_input_mute_state_changed)
]
)
end end
# define "on_" event methods. # define "on_" event methods.
def on_current_program_scene_changed def on_current_program_scene_changed(data)
... ...
end end
def on_input_mute_state_changed def on_input_mute_state_changed(data)
... ...
end end
... ...
@@ -111,10 +109,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 attributes {OBSWSRequestError}.req_name and {OBSWSRequestError}.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
To enable logs set an environmental variable `OBSWS_LOG_LEVEL` to the appropriate level.
example in powershell:
```powershell
$env:OBSWS_LOG_LEVEL="DEBUG"
```
### Tests ### Tests
To run all tests: To run all tests:

View File

@@ -1,9 +1,29 @@
require "minitest/test_task" require "minitest/test_task"
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
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,9 +0,0 @@
# frozen_string_literal: true
source "https://rubygems.org"
# gem "rails"
gem "perfect_toml", "~> 0.9.0"
gem "obsws", path: "../.."

View File

@@ -1,27 +0,0 @@
PATH
remote: ../..
specs:
obsws (0.0.2)
observer (~> 0.1.1)
waitutil (~> 0.2.1)
websocket-driver (~> 0.7.5)
GEM
remote: https://rubygems.org/
specs:
observer (0.1.1)
perfect_toml (0.9.0)
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!
perfect_toml (~> 0.9.0)
BUNDLED WITH
2.3.24

View File

@@ -1,9 +1,8 @@
require "obsws" require_relative "../../lib/obsws"
require "perfect_toml" require "yaml"
OBSWS::LOGGER.debug!
class Observer class Main
attr_reader :running attr_reader :running
def initialize(**kwargs) def initialize(**kwargs)
@@ -11,18 +10,20 @@ class Observer
@e_client = OBSWS::Events::Client.new(**kwargs) @e_client = OBSWS::Events::Client.new(**kwargs)
@e_client.add_observer(self) @e_client.add_observer(self)
puts info.join("\n") puts infostring
@running = true @running = true
end end
def info def run
sleep(0.1) while running
end
def infostring
resp = @r_client.get_version resp = @r_client.get_version
[ [
"Using obs version:", "Using obs version: #{resp.obs_version}.",
resp.obs_version, "With websocket version: #{resp.obs_web_socket_version}"
"With websocket version:", ].join(" ")
resp.obs_web_socket_version
]
end end
def on_current_program_scene_changed(data) def on_current_program_scene_changed(data)
@@ -45,14 +46,8 @@ class Observer
end end
end end
def conn_from_toml def conn_from_yaml
PerfectTOML.load_file("obs.toml", symbolize_names: true)[:connection] YAML.load_file("obs.yml", symbolize_names: true)[:connection]
end end
def main Main.new(**conn_from_yaml).run if $PROGRAM_NAME == __FILE__
o = Observer.new(**conn_from_toml)
sleep(0.1) while o.running
end
main if $0 == __FILE__

52
examples/levels/main.rb Normal file
View File

@@ -0,0 +1,52 @@
require_relative "../../lib/obsws"
require "yaml"
module LevelTypes
VU = 0
POSTFADER = 1
PREFADER = 2
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.register(
[
method(:on_input_mute_state_changed),
method(:on_input_volume_meters)
]
)
end
def run
puts "press <Enter> to quit"
loop { break if gets.chomp.empty? }
end
def on_input_mute_state_changed(data)
if data.input_name == DEVICE
puts "#{DEVICE} mute toggled"
end
end
def on_input_volume_meters(data)
fget = ->(x) { (x > 0) ? (20 * Math.log(x, 10)).round(1) : -200.0 }
data.inputs.each do |d|
name = d[:inputName]
if name == DEVICE && !d[:inputLevelsMul].empty?
left, right = d[:inputLevelsMul]
puts "#{name} [L: #{fget.call(left[LevelTypes::POSTFADER])}, R: #{fget.call(right[LevelTypes::POSTFADER])}]"
end
end
end
end
def conn_from_yaml
YAML.load_file("obs.yml", symbolize_names: true)[:connection]
end
Main.new(**conn_from_yaml).run if $PROGRAM_NAME == __FILE__

View File

@@ -0,0 +1,23 @@
require_relative "../../lib/obsws"
require "yaml"
class Main
def conn_from_yaml
YAML.load_file("obs.yml", symbolize_names: true)[:connection]
end
def run
OBSWS::Requests::Client.new(**conn_from_yaml).run do |client|
resp = client.get_scene_list
resp.scenes.reverse_each do |scene|
puts "Switching to scene #{scene[:sceneName]}"
client.set_current_program_scene(scene[:sceneName])
sleep(0.5)
end
end
end
end
Main.new.run if $PROGRAM_NAME == __FILE__

View File

@@ -1,11 +1,42 @@
require "digest/sha2"
require "json"
require "waitutil"
require "socket"
require "websocket/driver"
require "logger" require "logger"
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, msg)
@req_name = req_name
@code = code
@msg = msg
super(message)
end
def message
msg = [
"Request #{@req_name} returned code #{@code}."
]
msg << "With message: #{@msg}" if @msg
msg.join(" ")
end
end
end end

View File

@@ -1,72 +1,27 @@
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
attr_reader :url
def initialize(url, socket)
@url = url
@socket = socket
end
def write(s)
@socket.write(s)
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
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
@connect_timeout = kwargs[:connect_timeout] || 3
@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}] passing to handler")
msg_handler(JSON.parse(msg.data, symbolize_names: true))
end
start_driver
WaitUtil.wait_for_condition( WaitUtil.wait_for_condition(
"waiting successful identification", "successful identification",
delay_sec: 0.01, delay_sec: 0.01,
timeout_sec: 3 timeout_sec: @connect_timeout
) { @identified } ) { @identified }
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(
@@ -74,7 +29,7 @@ module OBSWS
) )
end end
def authenticate(auth = nil) def identify(auth)
payload = { payload = {
op: Mixin::OPCodes::IDENTIFY, op: Mixin::OPCodes::IDENTIFY,
d: { d: {
@@ -82,29 +37,28 @@ module OBSWS
eventSubscriptions: @subs eventSubscriptions: @subs
} }
} }
payload[:d][:authentication] = auth_token(**auth) if auth if auth
if @password.empty?
raise OBSWSError("auth enabled but no password provided")
end
logger.info("initiating authentication")
payload[:d][:authentication] = auth_token(**auth)
end
@driver.text(JSON.generate(payload)) @driver.text(JSON.generate(payload))
end end
def msg_handler(data) def msg_handler(data)
case data[:op] case data[:op]
when Mixin::OPCodes::HELLO when Mixin::OPCodes::HELLO
if data[:d].key? :authentication identify(data[:d][:authentication])
LOGGER.debug("initiating authentication")
else
LOGGER.debug("authentication disabled... skipping.")
end
authenticate(data[:d][:authentication])
when Mixin::OPCodes::IDENTIFIED when Mixin::OPCodes::IDENTIFIED
LOGGER.debug("client succesfully identified with server")
@identified = true @identified = true
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 +67,8 @@ module OBSWS
} }
} }
payload[:d][:requestData] = data if data payload[:d][:requestData] = data if data
queued = @driver.text(JSON.generate(payload)) logger.debug("sending request: #{payload}")
LOGGER.debug("request with id #{id} queued? #{queued}") @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,91 +1,94 @@
require "json"
require_relative "util"
require_relative "mixin"
module OBSWS module OBSWS
module Events module Events
module SUBS module SUBS
NONE = 0 NONE = 0
GENERAL = (1 << 0) GENERAL = 1 << 0
CONFIG = (1 << 1) CONFIG = 1 << 1
SCENES = (1 << 2) SCENES = 1 << 2
INPUTS = (1 << 3) INPUTS = 1 << 3
TRANSITIONS = (1 << 4) TRANSITIONS = 1 << 4
FILTERS = (1 << 5) FILTERS = 1 << 5
OUTPUTS = (1 << 6) OUTPUTS = 1 << 6
SCENEITEMS = (1 << 7) SCENEITEMS = 1 << 7
MEDIAINPUTS = (1 << 8) MEDIAINPUTS = 1 << 8
VENDORS = (1 << 9) VENDORS = 1 << 9
UI = (1 << 10) UI = 1 << 10
def low_volume LOW_VOLUME = GENERAL | CONFIG | SCENES | INPUTS | TRANSITIONS | FILTERS | OUTPUTS |
GENERAL | CONFIG | SCENES | INPUTS | TRANSITIONS | FILTERS | OUTPUTS |
SCENEITEMS | MEDIAINPUTS | VENDORS | UI SCENEITEMS | MEDIAINPUTS | VENDORS | UI
end
INPUTVOLUMEMETERS = (1 << 16) INPUTVOLUMEMETERS = 1 << 16
INPUTACTIVESTATECHANGED = (1 << 17) INPUTACTIVESTATECHANGED = 1 << 17
INPUTSHOWSTATECHANGED = (1 << 18) INPUTSHOWSTATECHANGED = 1 << 18
SCENEITEMTRANSFORMCHANGED = (1 << 19) SCENEITEMTRANSFORMCHANGED = 1 << 19
def high_volume HIGH_VOLUME = INPUTVOLUMEMETERS | INPUTACTIVESTATECHANGED | INPUTSHOWSTATECHANGED |
INPUTVOLUMEMETERS | INPUTACTIVESTATECHANGED | INPUTSHOWSTATECHANGED |
SCENEITEMTRANSFORMCHANGED SCENEITEMTRANSFORMCHANGED
end
def all ALL = LOW_VOLUME | HIGH_VOLUME
low_volume | high_volume
end
module_function :low_volume, :high_volume, :all
end end
module Callbacks module Callbacks
include Util include Util::String
def observers
@observers ||= []
end
def add_observer(observer) def add_observer(observer)
@observers = [] unless defined?(@observers) observer = [observer] unless observer.respond_to? :each
observer = [observer] if !observer.respond_to? :each observer.each { |o| observers << o unless observers.include? o }
observer.each { |o| @observers.append(o) }
end end
def remove_observer(observer) def remove_observer(observer)
@observers.delete(observer) observer = [observer] unless observer.respond_to? :each
observers.reject! { |o| observer.include? o }
end end
def notify_observers(event, data) private def notify_observers(event, data)
if defined?(@observers) observers.each do |o|
@observers.each do |o| if o.is_a? Method
if o.respond_to? "on_#{event.to_snake}" if o.name.to_s == "on_#{snakecase(event)}"
if data.empty? data.empty? ? o.call : o.call(data)
o.send("on_#{event.to_snake}") end
else elsif o.respond_to? "on_#{snakecase(event)}"
o.send("on_#{event.to_snake}", data) data.empty? ? o.send("on_#{snakecase(event)}") : o.send("on_#{snakecase(event)}", data)
end
end
end end
end end
end end
alias_method :callbacks, :observers
alias_method :register, :add_observer
alias_method :deregister, :remove_observer
end end
class Client class Client
include Logging
include Callbacks include Callbacks
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)
@base_client.add_observer(self) logger.info("#{self} successfully identified with server")
end rescue Errno::ECONNREFUSED, WaitUtil::TimeoutError => e
msg = "#{e.class.name}: #{e.message}"
def update(op_code, data) logger.error(msg)
raise OBSWSConnectionError.new(msg)
else
@base_client.updater = ->(op_code, data) {
if op_code == Mixin::OPCodes::EVENT if op_code == Mixin::OPCodes::EVENT
logger.debug("received: #{data}")
event = data[:eventType] event = data[:eventType]
data = data.key?(:eventData) ? data[:eventData] : {} data = data.fetch(:eventData, {})
notify_observers(event, Mixin::Data.new(data, data.keys)) notify_observers(event, Mixin::Data.new(data, data.keys))
end end
}
end
def to_s
self.class.name.split("::").last(2).join("::")
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

File diff suppressed because it is too large Load Diff

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,11 +7,11 @@ module OBSWS
end end
def minor def minor
0 5
end end
def patch def patch
3 4
end end
def to_a def to_a

15
main.rb
View File

@@ -1,17 +1,16 @@
require_relative "lib/obsws" require "obsws"
def main class Main
r_client = def run
OBSWS::Requests::Client.new( OBSWS::Requests::Client.new(
host: "localhost", host: "localhost",
port: 4455, port: 4455,
password: "strongpassword" password: "strongpassword"
) ).run do |client|
r_client.run do
# Toggle the mute state of your Mic input # Toggle the mute state of your Mic input
r_client.toggle_input_mute("Mic/Aux") client.toggle_input_mute("Mic/Aux")
end
end end
end end
main if $0 == __FILE__ Main.new.run if $PROGRAM_NAME == __FILE__

View File

@@ -1,10 +1,10 @@
# frozen_string_literal: true # frozen_string_literal: true
require File.expand_path("lib/obsws/version", __dir__) require File.expand_path("lib/obsws/version", __dir__)
lib = File.expand_path("./lib")
Gem::Specification.new do |spec| Gem::Specification.new do |spec|
spec.name = "obsws" spec.name = "obsws"
spec.version = OBSWS::Version spec.version = OBSWS::VERSION
spec.summary = "OBS Websocket v5 wrapper" spec.summary = "OBS Websocket v5 wrapper"
spec.description = "A Ruby wrapper around OBS Websocket v5" spec.description = "A Ruby wrapper around OBS Websocket v5"
spec.authors = ["onyx_online"] spec.authors = ["onyx_online"]
@@ -13,14 +13,13 @@ 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 "perfect_toml", "~> 0.9.0" spec.add_development_dependency "standard", "~> 1.30"
spec.add_development_dependency "minitest", "~> 5.16", ">= 5.16.3" 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", ">= 11.2.2"
spec.required_ruby_version = ">= 3.0" spec.required_ruby_version = ">= 3.0"
spec.metadata = { spec.metadata = {
"source_code_uri" => "https://github.com/onyx-and-iris/obsws.git" "source_code_uri" => "https://github.com/onyx-and-iris/obsws-ruby"
} }
end end

View File

@@ -1,17 +1,21 @@
require "minitest" require "minitest"
require "minitest/autorun" require "minitest/autorun"
require "perfect_toml" require "yaml"
require_relative "../lib/obsws" require_relative "../lib/obsws"
class OBSWSTest < Minitest::Test class OBSWSTest < Minitest::Test
def self.before_run class << self
conn = PerfectTOML.load_file("obs.toml", symbolize_names: true)[:connection] attr_reader :r_client
@@r_client = OBSWS::Requests::Client.new(**conn) end
@@r_client.create_scene("START_TEST") def self.before_run
@@r_client.create_scene("BRB_TEST") conn = YAML.load_file("obs.yml", symbolize_names: true)[:connection]
@@r_client.create_scene("END_TEST") @r_client = OBSWS::Requests::Client.new(**conn)
r_client.create_scene("START_TEST")
r_client.create_scene("BRB_TEST")
r_client.create_scene("END_TEST")
end end
before_run before_run
@@ -23,9 +27,9 @@ class OBSWSTest < Minitest::Test
end end
Minitest.after_run do Minitest.after_run do
@@r_client.remove_scene("START_TEST") r_client.remove_scene("START_TEST")
@@r_client.remove_scene("BRB_TEST") r_client.remove_scene("BRB_TEST")
@@r_client.remove_scene("END_TEST") r_client.remove_scene("END_TEST")
@@r_client.close r_client.close
end end
end end

View File

@@ -1,17 +0,0 @@
require_relative "../minitest_helper"
class AttrsTest < OBSWSTest
def test_get_version_attrs
resp = @@r_client.get_version
assert resp.attrs ==
%w[
available_requests
obs_version
obs_web_socket_version
platform
platform_description
rpc_version
supported_image_formats
]
end
end

17
test/obsws/test_attrs.rb Normal file
View File

@@ -0,0 +1,17 @@
require_relative "../minitest_helper"
class AttrsTest < OBSWSTest
def test_get_version_attrs
resp = OBSWSTest.r_client.get_version
assert resp.attrs ==
%w[
available_requests
obs_version
obs_web_socket_version
platform
platform_description
rpc_version
supported_image_formats
]
end
end

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

@@ -0,0 +1,17 @@
require_relative "../minitest_helper"
class OBSWSConnectionErrorTest < Minitest::Test
def test_it_raises_an_obsws_connection_error_on_wrong_password
e = assert_raises(OBSWS::OBSWSConnectionError) { OBSWS::Requests::Client.new(host: "localhost", port: 4455, password: "wrongpassword", connect_timeout: 1).new }
assert_equal(e.message, "Timed out waiting for successful identification (1 seconds elapsed)")
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(e.req_name, "ToggleInputMute")
assert_equal(e.code, 600)
assert_equal(e.message, "Request ToggleInputMute returned code 600. With message: No source was found by the name of `unknown`.")
end
end

View File

@@ -2,21 +2,21 @@ require_relative "../minitest_helper"
class RequestTest < OBSWSTest class RequestTest < OBSWSTest
def test_it_checks_obs_major_version def test_it_checks_obs_major_version
resp = @@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)
assert ver[0] >= 28 assert ver[0] >= 28
end end
def test_it_checks_ws_major_version def test_it_checks_ws_major_version
resp = @@r_client.get_version resp = OBSWSTest.r_client.get_version
ver = resp.obs_web_socket_version.split(".").map(&:to_i) ver = resp.obs_web_socket_version.split(".").map(&:to_i)
assert ver[0] >= 5 assert ver[0] >= 5
end end
def test_it_sets_and_gets_current_program_scene def test_it_sets_and_gets_current_program_scene
%w[START_TEST BRB_TEST END_TEST].each do |s| %w[START_TEST BRB_TEST END_TEST].each do |s|
@@r_client.set_current_program_scene(s) OBSWSTest.r_client.set_current_program_scene(s)
resp = @@r_client.get_current_program_scene resp = OBSWSTest.r_client.get_current_program_scene
assert resp.current_program_scene_name == s assert resp.current_program_scene_name == s
end end
end end
@@ -26,8 +26,8 @@ class RequestTest < OBSWSTest
server: "rtmp://addressofrtmpserver", server: "rtmp://addressofrtmpserver",
key: "live_myvery_secretkey" key: "live_myvery_secretkey"
} }
@@r_client.set_stream_service_settings("rtmp_common", settings) OBSWSTest.r_client.set_stream_service_settings("rtmp_common", settings)
resp = @@r_client.get_stream_service_settings resp = OBSWSTest.r_client.get_stream_service_settings
assert resp.stream_service_type == "rtmp_common" assert resp.stream_service_type == "rtmp_common"
assert resp.stream_service_settings == assert resp.stream_service_settings ==
{ {