obs-websocket 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +6 -0
- data/HACKING.md +13 -0
- data/LICENSE +21 -0
- data/README.md +7 -0
- data/examples/websocket-driver/Gemfile +4 -0
- data/examples/websocket-driver/Gemfile.lock +23 -0
- data/examples/websocket-driver/main.rb +89 -0
- data/lib/obs-websocket.rb +3 -0
- data/lib/obs/websocket.rb +443 -0
- data/lib/obs/websocket/protocol.rb +4380 -0
- data/lib/obs/websocket/version.rb +7 -0
- data/obs-websocket.gemspec +32 -0
- metadata +72 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 60bbb3f01fc0e188ac097d94be7e06b44c75ff002fbe472a3a1ab250c546c466
|
4
|
+
data.tar.gz: b09a363ab96faa4cfd516d5f76f6f5d101e4b562be3bb6d53deddc5b4c2db20c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7bc1716e17df1452b8a666c4362b64189d64825f1a114348c9625cb8420601f28022b32ed1379d539c694575e84905ee8e127d017be6c6255975a2d4ef06ec77
|
7
|
+
data.tar.gz: 6643bb0a4b17ca827a5e31ff2e30560adaa05babebbdb10932ab9dba44465a2c6f5c443cede4d8ab1a10c6e2472be95a41f656d7443f421fd098a55c36b4e8ca
|
data/CHANGELOG.md
ADDED
data/HACKING.md
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# Hacking ruby-obs-websocket
|
2
|
+
|
3
|
+
All contributions including patches and bug reports are welcome at GitHub <https://github.com/hanazuki/ruby-obs-websocket>.
|
4
|
+
|
5
|
+
## Packaging and release
|
6
|
+
|
7
|
+
### Update obs/websocket/protocol.rb
|
8
|
+
|
9
|
+
When a new version of obs-websocket is released or the codegen script is modified, take the following steps to generate the updated protocol definition.
|
10
|
+
|
11
|
+
1. Change `API_VERSION` in `Rakefile`.
|
12
|
+
2. Run `rake codegen`
|
13
|
+
3. Commit (and release).
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2021 Kasumi Hanazuki
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
# OBS::WebSocket
|
2
|
+
|
3
|
+
OBS::WebSocket is a [Ruby](https://www.ruby-lang.org) library to remotely controls [OBS Studio](https://obsproject.com/) using [obs-websocket](https://github.com/Palakis/obs-websocket) API.
|
4
|
+
|
5
|
+
## Usage
|
6
|
+
|
7
|
+
See examples/ for a runnable code example.
|
@@ -0,0 +1,23 @@
|
|
1
|
+
PATH
|
2
|
+
remote: ../..
|
3
|
+
specs:
|
4
|
+
obs-websocket (0.1.0)
|
5
|
+
concurrent-ruby (~> 1.1)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
concurrent-ruby (1.1.8)
|
11
|
+
websocket-driver (0.7.3)
|
12
|
+
websocket-extensions (>= 0.1.0)
|
13
|
+
websocket-extensions (0.1.5)
|
14
|
+
|
15
|
+
PLATFORMS
|
16
|
+
x86_64-linux
|
17
|
+
|
18
|
+
DEPENDENCIES
|
19
|
+
obs-websocket!
|
20
|
+
websocket-driver
|
21
|
+
|
22
|
+
BUNDLED WITH
|
23
|
+
2.2.15
|
@@ -0,0 +1,89 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# SPDX-License-Identifier: MIT
|
3
|
+
|
4
|
+
ENV['BUNDLE_GEMFILE'] = File.join(__dir__, 'Gemfile')
|
5
|
+
require 'bundler/setup'
|
6
|
+
|
7
|
+
require 'obs/websocket'
|
8
|
+
require 'socket'
|
9
|
+
require 'uri'
|
10
|
+
require 'websocket/driver'
|
11
|
+
|
12
|
+
class Main
|
13
|
+
def run(uri, password)
|
14
|
+
connect(URI.parse(uri))
|
15
|
+
|
16
|
+
# To initialize OBS::WebSocket, pass a WebSocket connection.
|
17
|
+
obs = OBS::WebSocket::Client.new(@driver)
|
18
|
+
|
19
|
+
obs.on_open do
|
20
|
+
obs.authenticate!(password).value!
|
21
|
+
|
22
|
+
# Request methods return Future values that can be waited for by `value!`.
|
23
|
+
version = obs.get_version.value!
|
24
|
+
puts "OBS version: #{version.obs_studio_version}; OBS-websocket version: #{version.obs_websocket_version}"
|
25
|
+
|
26
|
+
# Pass request parameters using keyword arguments. Composite types are mapped to Hash objects.
|
27
|
+
obs.broadcast_custom_message(
|
28
|
+
realm: 'helloworld',
|
29
|
+
data: {greeting: 'Hello, World!'}
|
30
|
+
).wait!
|
31
|
+
rescue => e
|
32
|
+
$stderr.puts e.message
|
33
|
+
obs.close
|
34
|
+
end
|
35
|
+
|
36
|
+
# Listen for events using `on_*` methods. The event payload is yielded to the block.
|
37
|
+
obs.on_broadcast_custom_message do |ev|
|
38
|
+
puts ev.data['greeting']
|
39
|
+
obs.close
|
40
|
+
end
|
41
|
+
|
42
|
+
obs.on_close(executor: :immediate) do |code, reason|
|
43
|
+
puts "Bye! (code: #{code})"
|
44
|
+
end
|
45
|
+
|
46
|
+
start_driver
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def connect(uri)
|
52
|
+
fail ArgumentError, 'Only supports ws:// URI' unless uri.scheme == 'ws'
|
53
|
+
|
54
|
+
@socket = TCPSocket.new(uri.host, uri.port || 80)
|
55
|
+
@driver = WebSocket::Driver.client(SocketWrapper.new(uri.to_s, @socket))
|
56
|
+
end
|
57
|
+
|
58
|
+
def start_driver
|
59
|
+
@driver.start
|
60
|
+
|
61
|
+
loop do
|
62
|
+
@driver.parse(@socket.readpartial(4096))
|
63
|
+
rescue EOFError
|
64
|
+
break
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class SocketWrapper
|
69
|
+
def initialize(url, socket)
|
70
|
+
@url = url
|
71
|
+
@socket = socket
|
72
|
+
end
|
73
|
+
|
74
|
+
attr_reader :url
|
75
|
+
|
76
|
+
def write(s)
|
77
|
+
@socket.write(s)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
if $0 == __FILE__
|
83
|
+
if ARGV.size != 2
|
84
|
+
$stderr.puts "Usage: #{File.basename __FILE__} ws://HOST:PORT PASSWORD"
|
85
|
+
exit 1
|
86
|
+
end
|
87
|
+
|
88
|
+
Main.new.run(*ARGV)
|
89
|
+
end
|
@@ -0,0 +1,443 @@
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
2
|
+
|
3
|
+
require 'concurrent'
|
4
|
+
require 'digest/sha2'
|
5
|
+
require 'json'
|
6
|
+
require 'securerandom'
|
7
|
+
|
8
|
+
require_relative 'websocket/version'
|
9
|
+
|
10
|
+
module OBS
|
11
|
+
module WebSocket
|
12
|
+
class Error < StandardError; end
|
13
|
+
|
14
|
+
class RequestError < Error; end
|
15
|
+
|
16
|
+
# Shortcut for {OBS::WebSocket::Client#initialize}
|
17
|
+
#
|
18
|
+
# @see OBS::WebSocket::Client#initialize
|
19
|
+
def self.new(*args, **kwargs)
|
20
|
+
Client.new(*args, **kwargs)
|
21
|
+
end
|
22
|
+
|
23
|
+
# OBS-websocket client.
|
24
|
+
class Client
|
25
|
+
# Creates an OBS-websocket client.
|
26
|
+
#
|
27
|
+
# <tt>websocket</tt> object must respond to the following methods:
|
28
|
+
# - <tt>text(str)</tt>: send a text frame
|
29
|
+
# - <tt>on(event, &block)</tt>: add an event handler
|
30
|
+
# - <tt>close()</tt>: close the connection
|
31
|
+
#
|
32
|
+
# @param websocket [Object] the websocket object
|
33
|
+
# @param executor the executor on which the callbacks are invoked
|
34
|
+
def initialize(websocket, executor: :io)
|
35
|
+
@websocket = websocket
|
36
|
+
@response_dispatcher = ResponseDispatcher.new
|
37
|
+
@event_dispatcher = EventDispatcher.new
|
38
|
+
@executor = executor
|
39
|
+
@on_open = Concurrent::Promises.resolvable_event
|
40
|
+
@on_close = Concurrent::Promises.resolvable_future
|
41
|
+
|
42
|
+
websocket.on(:open) do
|
43
|
+
@on_open.resolve
|
44
|
+
end
|
45
|
+
|
46
|
+
websocket.on(:close) do |event|
|
47
|
+
@on_close.resolve(true, [event.code, event.reason])
|
48
|
+
end
|
49
|
+
|
50
|
+
websocket.on(:message) do |event|
|
51
|
+
handle_message(JSON.parse(event.data))
|
52
|
+
end
|
53
|
+
|
54
|
+
websocket.on(:error) do |event|
|
55
|
+
$stderr.puts "Error: #{event.code} #{event.reason}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Authenticates the client to the server using the password.
|
60
|
+
#
|
61
|
+
# @param password [String] the password
|
62
|
+
# @return [Future<:ok>]
|
63
|
+
def authenticate!(password)
|
64
|
+
get_auth_required.then do |h|
|
65
|
+
if h.auth_required
|
66
|
+
token = auth_token(
|
67
|
+
password: password,
|
68
|
+
salt: h.salt,
|
69
|
+
challenge: h.challenge,
|
70
|
+
)
|
71
|
+
authenticate(auth: token).then { :ok }
|
72
|
+
else
|
73
|
+
:ok
|
74
|
+
end
|
75
|
+
end.flat
|
76
|
+
end
|
77
|
+
|
78
|
+
# Adds an event handler for connection establishment.
|
79
|
+
#
|
80
|
+
# @param executor the executor on which the callback is invoked
|
81
|
+
# @yield Called when obs-websocket connection is established.
|
82
|
+
# @return [Event]
|
83
|
+
def on_open(executor: @executor, &listener)
|
84
|
+
if listener
|
85
|
+
@on_open.chain_on(executor, &listener)
|
86
|
+
else
|
87
|
+
@on_open.with_default_executor(executor)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Adds an event handler for connection termination.
|
92
|
+
#
|
93
|
+
# @param executor the executor on which the callback is invoked
|
94
|
+
# @yield Called when obs-websocket connection is terminated.
|
95
|
+
# @return [Future]
|
96
|
+
def on_close(executor: @executor, &listener)
|
97
|
+
if listener
|
98
|
+
@on_close.then_on(executor, &listener)
|
99
|
+
else
|
100
|
+
@on_close.with_default_executor(executor)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Adds an event handler for obs-websocket event.
|
105
|
+
#
|
106
|
+
# @param type [String] type of obs-websocket event to listen for
|
107
|
+
# @param executor the executor on which the callback is invoked
|
108
|
+
# @yield Called when the specified type of obs-websocket event is received.
|
109
|
+
# @yieldparam event [Event] the event object
|
110
|
+
# @return [void]
|
111
|
+
def on(type, executor: @executor, &listener)
|
112
|
+
@event_dispatcher.register(executor, type, listener)
|
113
|
+
nil
|
114
|
+
end
|
115
|
+
|
116
|
+
# Close the connection.
|
117
|
+
#
|
118
|
+
# @return [void]
|
119
|
+
def close
|
120
|
+
@websocket.close
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
def auth_token(password:, salt:, challenge:)
|
126
|
+
Digest::SHA256.base64digest(Digest::SHA256.base64digest(password + salt) + challenge)
|
127
|
+
end
|
128
|
+
|
129
|
+
def send_request(request)
|
130
|
+
message_id, future = @response_dispatcher.register(@executor, request)
|
131
|
+
@websocket.text(JSON.dump({**request.to_h, 'message-id' => message_id}))
|
132
|
+
future
|
133
|
+
end
|
134
|
+
|
135
|
+
def handle_message(payload)
|
136
|
+
if message_id = payload['message-id']
|
137
|
+
@response_dispatcher.dispatch(message_id, payload)
|
138
|
+
elsif update_type = payload['update-type']
|
139
|
+
@event_dispatcher.dispatch(update_type, payload)
|
140
|
+
else
|
141
|
+
fail 'Unknown message'
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
class EventDispatcher
|
147
|
+
def initialize
|
148
|
+
@listeners = Hash.new {|h, k| h[k] = []}
|
149
|
+
end
|
150
|
+
|
151
|
+
def register(executor, type, listener)
|
152
|
+
@listeners[type].push([executor, listener])
|
153
|
+
end
|
154
|
+
|
155
|
+
def dispatch(update_type, payload)
|
156
|
+
event = Protocol::Event.create(update_type, payload).freeze
|
157
|
+
@listeners[update_type].each do |(executor, listener)|
|
158
|
+
Concurrent::Promises.future_on(executor, event, &listener).run
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
class ResponseDispatcher
|
164
|
+
def initialize
|
165
|
+
@ongoing_requests = {}
|
166
|
+
end
|
167
|
+
|
168
|
+
def register(executor, request)
|
169
|
+
message_id = new_message_id
|
170
|
+
future = Concurrent::Promises.resolvable_future_on(executor)
|
171
|
+
|
172
|
+
@ongoing_requests[message_id] = {
|
173
|
+
request: request,
|
174
|
+
future: future,
|
175
|
+
}
|
176
|
+
|
177
|
+
[message_id, future.with_hidden_resolvable]
|
178
|
+
end
|
179
|
+
|
180
|
+
def dispatch(message_id, payload)
|
181
|
+
if h = @ongoing_requests.delete(message_id)
|
182
|
+
request = h[:request]
|
183
|
+
future = h[:future]
|
184
|
+
|
185
|
+
case payload['status']
|
186
|
+
when 'ok'
|
187
|
+
response_class = request.class.const_get(:Response)
|
188
|
+
future.fulfill(response_class.new(payload))
|
189
|
+
when 'error'
|
190
|
+
future.reject(RequestError.new(payload['error']))
|
191
|
+
else
|
192
|
+
fail 'Unknown status'
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
private
|
198
|
+
|
199
|
+
def new_message_id
|
200
|
+
SecureRandom.uuid
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
module Protocol
|
205
|
+
class Type
|
206
|
+
def initialize(**kwargs, &block)
|
207
|
+
kwargs.each do |k, v|
|
208
|
+
instance_variable_set(:"@#{k}", v)
|
209
|
+
end
|
210
|
+
instance_eval(&block)
|
211
|
+
end
|
212
|
+
|
213
|
+
attr_reader :name
|
214
|
+
end
|
215
|
+
|
216
|
+
module Types
|
217
|
+
Boolean = Type.new(name: 'Boolean') do
|
218
|
+
def as_ruby(b)
|
219
|
+
!!b
|
220
|
+
end
|
221
|
+
|
222
|
+
def as_json(b)
|
223
|
+
!!b
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
Integer = Type.new(name: 'Int') do
|
228
|
+
def as_ruby(i)
|
229
|
+
i.to_i
|
230
|
+
end
|
231
|
+
|
232
|
+
def as_json(i)
|
233
|
+
i.to_i
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
Float = Type.new(name: 'Double') do
|
238
|
+
def as_ruby(f)
|
239
|
+
f.to_f
|
240
|
+
end
|
241
|
+
|
242
|
+
def as_json(f)
|
243
|
+
f.to_f
|
244
|
+
end
|
245
|
+
end
|
246
|
+
Numeric = Float
|
247
|
+
|
248
|
+
String = Type.new(name: 'String') do
|
249
|
+
def as_ruby(s)
|
250
|
+
s.to_s
|
251
|
+
end
|
252
|
+
|
253
|
+
def as_json(s)
|
254
|
+
s.to_s
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
Optional = Class.new do
|
259
|
+
def [](element_type)
|
260
|
+
Type.new(name: "Optional[#{element_type.name}]", element_type: element_type) do
|
261
|
+
def as_ruby(v)
|
262
|
+
return unless v
|
263
|
+
@element_type.as_ruby(v)
|
264
|
+
end
|
265
|
+
|
266
|
+
def as_json(v)
|
267
|
+
return unless v
|
268
|
+
@element_type.as_json(v)
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end.new
|
273
|
+
|
274
|
+
Array = Class.new do
|
275
|
+
def [](element_type)
|
276
|
+
Type.new(name: "Array[#{element_type.name}]", element_type: element_type) do
|
277
|
+
def as_ruby(a)
|
278
|
+
a.to_a.map {|v| @element_type.as_ruby(v) }
|
279
|
+
end
|
280
|
+
|
281
|
+
def as_json(a)
|
282
|
+
a.to_a.map {|v| @element_type.as_json(v) }
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end.new
|
287
|
+
|
288
|
+
Object = Type.new(name: 'Object') do
|
289
|
+
def as_ruby(o)
|
290
|
+
o.to_h
|
291
|
+
end
|
292
|
+
|
293
|
+
def as_json(o)
|
294
|
+
o.to_h
|
295
|
+
end
|
296
|
+
|
297
|
+
def [](fields)
|
298
|
+
Type.new(name: "Object[#{fields.keys.map(&:to_s).join(', ')}]", fields: fields) do
|
299
|
+
def as_ruby(o)
|
300
|
+
@fields.to_h do |name, field|
|
301
|
+
json_name = field[:json_name]
|
302
|
+
type = field[:type]
|
303
|
+
[name, type.as_ruby(o[json_name])]
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
def as_json(a)
|
308
|
+
@fields.to_h do |name, field|
|
309
|
+
json_name = field[:json_name]
|
310
|
+
type = field[:type]
|
311
|
+
[json_name, type.as_ruby(o[name])]
|
312
|
+
end
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
StringOrObject = Type.new(name: 'Object') do
|
319
|
+
def as_ruby(o)
|
320
|
+
String === o ? o : o.to_h
|
321
|
+
end
|
322
|
+
|
323
|
+
def as_json(o)
|
324
|
+
o.respond_to?(:to_str) ? o.to_str : o.to_h
|
325
|
+
end
|
326
|
+
|
327
|
+
def [](fields)
|
328
|
+
Type.new(name: "Object[#{fields.keys.map(&:to_s).join(', ')}]", fields: fields) do
|
329
|
+
def as_ruby(o)
|
330
|
+
return o if String === o
|
331
|
+
|
332
|
+
@fields.to_h do |name, field|
|
333
|
+
json_name = field[:json_name]
|
334
|
+
type = field[:type]
|
335
|
+
[name, type.as_ruby(o[json_name])]
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
def as_json(a)
|
340
|
+
return o.to_str if o.respond_to?(:to_str)
|
341
|
+
|
342
|
+
@fields.to_h do |name, field|
|
343
|
+
json_name = field[:json_name]
|
344
|
+
type = field[:type]
|
345
|
+
[json_name, type.as_ruby(o[name])]
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
class ServerMessage
|
354
|
+
end
|
355
|
+
|
356
|
+
class ClientMessage
|
357
|
+
end
|
358
|
+
|
359
|
+
class Event < ServerMessage
|
360
|
+
CLASSES_BY_JSON_NAME = {}
|
361
|
+
private_constant :CLASSES_BY_JSON_NAME
|
362
|
+
|
363
|
+
def self.json_name(json_name)
|
364
|
+
CLASSES_BY_JSON_NAME[json_name] = self
|
365
|
+
end
|
366
|
+
|
367
|
+
def self.create(type, payload)
|
368
|
+
cls = CLASSES_BY_JSON_NAME[type] || UnknownEvent
|
369
|
+
cls.new(payload)
|
370
|
+
end
|
371
|
+
|
372
|
+
def initialize(json)
|
373
|
+
@json = json
|
374
|
+
end
|
375
|
+
|
376
|
+
private def get_field(name, type)
|
377
|
+
type.as_ruby(@json[name])
|
378
|
+
end
|
379
|
+
|
380
|
+
def to_h
|
381
|
+
@json
|
382
|
+
end
|
383
|
+
|
384
|
+
def update_type
|
385
|
+
get_field('update-type', Types::String)
|
386
|
+
end
|
387
|
+
def stream_timecode;
|
388
|
+
get_field('stream-timecode', Types::Optional[Types::String])
|
389
|
+
end
|
390
|
+
def rec_timecode
|
391
|
+
get_field('rec-timecode', Types::Optional[Types::String])
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
class UnknownEvent < Event
|
396
|
+
end
|
397
|
+
|
398
|
+
class Request < ClientMessage
|
399
|
+
class << self
|
400
|
+
def json_name(json_name)
|
401
|
+
@json_name = json_name
|
402
|
+
end
|
403
|
+
|
404
|
+
def params(params = {})
|
405
|
+
(@params ||= {}).update(params)
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
def initialize(args)
|
410
|
+
@json = self.class.instance_variable_get(:@params).to_h do |name, v|
|
411
|
+
type = v[:type]
|
412
|
+
json_name = v[:json_name]
|
413
|
+
[json_name, type.as_json(args[name])]
|
414
|
+
end
|
415
|
+
@json['request-type'] = self.class.instance_variable_get(:@json_name)
|
416
|
+
end
|
417
|
+
|
418
|
+
def to_h
|
419
|
+
@json
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
class Response < ServerMessage
|
424
|
+
def initialize(json)
|
425
|
+
@json = json
|
426
|
+
end
|
427
|
+
|
428
|
+
def to_h
|
429
|
+
@json
|
430
|
+
end
|
431
|
+
|
432
|
+
private def get_field(name, type)
|
433
|
+
type.as_ruby(@json[name])
|
434
|
+
end
|
435
|
+
end
|
436
|
+
end
|
437
|
+
|
438
|
+
require_relative 'websocket/protocol'
|
439
|
+
|
440
|
+
Client.include Protocol::Event::Mixin
|
441
|
+
Client.include Protocol::Request::Mixin
|
442
|
+
end
|
443
|
+
end
|