discorb-voice 0.1.0 → 0.1.1
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 +4 -4
- data/.github/workflows/lint-push.yml +20 -0
- data/.github/workflows/lint.yml +16 -0
- data/.rubocop.yml +77 -0
- data/discorb-voice.gemspec +1 -1
- data/lib/discorb/voice/core.rb +326 -314
- data/lib/discorb/voice/error.rb +10 -7
- data/lib/discorb/voice/extend.rb +79 -64
- data/lib/discorb/voice/ogg.rb +121 -120
- data/lib/discorb/voice/source.rb +113 -110
- data/lib/discorb/voice/version.rb +1 -1
- data/lib/discorb/voice.rb +13 -13
- data/lib/discorb-voice.rb +2 -1
- metadata +7 -4
data/lib/discorb/voice/core.rb
CHANGED
@@ -1,314 +1,326 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "async"
|
4
|
-
require "async/websocket"
|
5
|
-
require "rbnacl"
|
6
|
-
require "socket"
|
7
|
-
|
8
|
-
module Discorb
|
9
|
-
module
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
# @
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
@
|
34
|
-
@
|
35
|
-
@
|
36
|
-
@
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
#
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
stream
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
# puts packet
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
next_time
|
103
|
-
#
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
#
|
116
|
-
#
|
117
|
-
# #
|
118
|
-
#
|
119
|
-
#
|
120
|
-
#
|
121
|
-
#
|
122
|
-
#
|
123
|
-
# @
|
124
|
-
#
|
125
|
-
|
126
|
-
#
|
127
|
-
|
128
|
-
# #
|
129
|
-
#
|
130
|
-
#
|
131
|
-
#
|
132
|
-
#
|
133
|
-
#
|
134
|
-
|
135
|
-
#
|
136
|
-
|
137
|
-
#
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
#
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
@
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
end
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
@
|
216
|
-
|
217
|
-
rescue
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
@
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
@
|
247
|
-
|
248
|
-
@
|
249
|
-
|
250
|
-
@
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
@
|
266
|
-
@
|
267
|
-
when
|
268
|
-
@
|
269
|
-
@
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "async"
|
4
|
+
require "async/websocket"
|
5
|
+
require "rbnacl"
|
6
|
+
require "socket"
|
7
|
+
|
8
|
+
module Discorb
|
9
|
+
module Voice
|
10
|
+
OPUS_SAMPLE_RATE = 48_000
|
11
|
+
OPUS_FRAME_LENGTH = 20
|
12
|
+
|
13
|
+
#
|
14
|
+
# Client for voice connection.
|
15
|
+
#
|
16
|
+
class Client
|
17
|
+
# @private
|
18
|
+
attr_reader :connect_condition
|
19
|
+
# @return [:connecting, :connected, :closed, :ready, :reconnecting] The current status of the voice connection
|
20
|
+
attr_reader :status
|
21
|
+
# @return [:stopped, :playing] The current status of playing audio
|
22
|
+
attr_reader :playing_status
|
23
|
+
# @return [Async::Condition] The condition of playing audio
|
24
|
+
attr_reader :playing_condition
|
25
|
+
|
26
|
+
# @private
|
27
|
+
def initialize(client, data)
|
28
|
+
@client = client
|
29
|
+
@token = data[:token]
|
30
|
+
@guild_id = data[:guild_id]
|
31
|
+
@endpoint = data[:endpoint]
|
32
|
+
@status = :connecting
|
33
|
+
@playing_status = :stopped
|
34
|
+
@connect_condition = Async::Condition.new
|
35
|
+
@paused_condition = Async::Condition.new
|
36
|
+
@play_condition = Async::Condition.new
|
37
|
+
Async do
|
38
|
+
start_receive false
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
#
|
43
|
+
# Sends a speaking indicator to the server.
|
44
|
+
#
|
45
|
+
# @param [Boolean] high_priority Whether to send audio in high priority.
|
46
|
+
#
|
47
|
+
def speaking(high_priority: false)
|
48
|
+
flag = 1
|
49
|
+
flag |= 1 << 2 if high_priority
|
50
|
+
send_connection_message(5, {
|
51
|
+
speaking: flag,
|
52
|
+
delay: 0,
|
53
|
+
ssrc: @ssrc,
|
54
|
+
})
|
55
|
+
end
|
56
|
+
|
57
|
+
def stop_speaking
|
58
|
+
send_connection_message(5, {
|
59
|
+
speaking: false,
|
60
|
+
delay: 0,
|
61
|
+
ssrc: @ssrc,
|
62
|
+
})
|
63
|
+
end
|
64
|
+
|
65
|
+
#
|
66
|
+
# Plays audio from a source.
|
67
|
+
#
|
68
|
+
# @param [Discorb::Voice::Source] source data The audio source
|
69
|
+
# @param [Boolean] high_priority Whether to play audio in high priority
|
70
|
+
#
|
71
|
+
def play(source, high_priority: false)
|
72
|
+
@playing_task = Async do
|
73
|
+
speaking(high_priority: high_priority)
|
74
|
+
@playing_status = :playing
|
75
|
+
@playing_condition = Async::Condition.new
|
76
|
+
stream = OggStream.new(source.io)
|
77
|
+
loops = 0
|
78
|
+
@start_time = Time.now.to_f
|
79
|
+
delay = OPUS_FRAME_LENGTH / 1000.0
|
80
|
+
|
81
|
+
stream.packets.each_with_index do |packet, _i|
|
82
|
+
if @playing_status == :stopped
|
83
|
+
source.cleanup
|
84
|
+
break
|
85
|
+
elsif @playing_status == :paused
|
86
|
+
@paused_condition.wait
|
87
|
+
elsif @status != :ready
|
88
|
+
sleep 0.02 while @status != :ready
|
89
|
+
|
90
|
+
speaking(high_priority: high_priority)
|
91
|
+
end
|
92
|
+
# p i
|
93
|
+
@timestamp += (OPUS_SAMPLE_RATE / 1000.0 * OPUS_FRAME_LENGTH).to_i
|
94
|
+
@sequence += 1
|
95
|
+
# puts packet.data[...10].unpack1("H*")
|
96
|
+
# puts packet[-10..]&.unpack1("H*")
|
97
|
+
send_audio(packet)
|
98
|
+
# puts "Sent packet #{i}"
|
99
|
+
loops += 1
|
100
|
+
next_time = @start_time + (delay * (loops + 1))
|
101
|
+
# p [next_time, Time.now.to_f, delay]
|
102
|
+
sleep(next_time - Time.now.to_f) if next_time > Time.now.to_f
|
103
|
+
# @voice_connection.flush
|
104
|
+
end
|
105
|
+
# p :e
|
106
|
+
@playing_status = :stopped
|
107
|
+
@playing_condition.signal
|
108
|
+
source.cleanup
|
109
|
+
stop_speaking
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# NOTE: This is commented out because it raises an error.
|
114
|
+
# It's not clear why this is happening.
|
115
|
+
# #
|
116
|
+
# # Pause playing audio.
|
117
|
+
# #
|
118
|
+
# def pause
|
119
|
+
# raise VoiceError, "Not playing" unless @playing_status == :playing
|
120
|
+
# send_audio(OPUS_SILENCE)
|
121
|
+
# @paused_condition = Async::Condition.new
|
122
|
+
# @paused_offset = Time.now.to_f - @start_time
|
123
|
+
# @playing_status = :paused
|
124
|
+
# end
|
125
|
+
|
126
|
+
# #
|
127
|
+
# # Resumes playing audio.
|
128
|
+
# #
|
129
|
+
# def resume
|
130
|
+
# raise VoiceError, "Not paused" unless @playing_status == :paused
|
131
|
+
# @paused_condition.signal
|
132
|
+
# @start_time = Time.now.to_f - @paused_offset
|
133
|
+
# end
|
134
|
+
|
135
|
+
#
|
136
|
+
# Stop playing audio.
|
137
|
+
#
|
138
|
+
def stop
|
139
|
+
@playing_status = :stopped
|
140
|
+
send_audio(OPUS_SILENCE)
|
141
|
+
end
|
142
|
+
|
143
|
+
#
|
144
|
+
# Disconnects from the voice server.
|
145
|
+
#
|
146
|
+
def disconnect
|
147
|
+
begin
|
148
|
+
@connection.close
|
149
|
+
rescue StandardError
|
150
|
+
nil
|
151
|
+
end
|
152
|
+
@client.disconnect_voice(@guild_id)
|
153
|
+
cleanup
|
154
|
+
end
|
155
|
+
|
156
|
+
private
|
157
|
+
|
158
|
+
OPUS_SILENCE = [0xF8, 0xFF, 0xFE].pack("C*")
|
159
|
+
|
160
|
+
def cleanup
|
161
|
+
@heartbeat_task&.stop
|
162
|
+
|
163
|
+
@voice_connection&.close
|
164
|
+
end
|
165
|
+
|
166
|
+
def send_audio(data)
|
167
|
+
header = create_header
|
168
|
+
@voice_connection.send(
|
169
|
+
header + encrypt_audio(
|
170
|
+
data,
|
171
|
+
header
|
172
|
+
),
|
173
|
+
0
|
174
|
+
# @sockaddr
|
175
|
+
)
|
176
|
+
rescue IOError
|
177
|
+
@client.logger.warn("Voice UDP connection closed")
|
178
|
+
@playing_task.stop if @status != :closed
|
179
|
+
end
|
180
|
+
|
181
|
+
def start_receive(resume)
|
182
|
+
Async do
|
183
|
+
@client.voice_mutexes[@guild_id] ||= Mutex.new
|
184
|
+
next if @client.voice_mutexes[@guild_id].locked?
|
185
|
+
@client.voice_mutexes[@guild_id].synchronize do
|
186
|
+
endpoint = Async::HTTP::Endpoint.parse("wss://" + @endpoint + "?v=4", alpn_protocols: Async::HTTP::Protocol::HTTP11.names)
|
187
|
+
@client.logger.info("Connecting to #{endpoint}")
|
188
|
+
@connection = Async::WebSocket::Client.connect(endpoint, handler: Discorb::Gateway::RawConnection)
|
189
|
+
@status = :connected
|
190
|
+
if resume
|
191
|
+
send_connection_message(
|
192
|
+
7,
|
193
|
+
{
|
194
|
+
server_id: @guild_id,
|
195
|
+
session_id: @client.session_id,
|
196
|
+
token: @token,
|
197
|
+
}
|
198
|
+
)
|
199
|
+
else
|
200
|
+
send_connection_message(
|
201
|
+
0,
|
202
|
+
{
|
203
|
+
server_id: @guild_id,
|
204
|
+
user_id: @client.user.id,
|
205
|
+
session_id: @client.session_id,
|
206
|
+
token: @token,
|
207
|
+
}
|
208
|
+
)
|
209
|
+
end
|
210
|
+
while (raw_message = @connection.read)
|
211
|
+
message = JSON.parse(raw_message, symbolize_names: true)
|
212
|
+
handle_voice_connection(message)
|
213
|
+
end
|
214
|
+
rescue Async::Wrapper::Cancelled
|
215
|
+
@status = :closed
|
216
|
+
cleanup
|
217
|
+
rescue Errno::EPIPE, EOFError
|
218
|
+
@status = :reconnecting
|
219
|
+
@connect_condition = Async::Condition.new
|
220
|
+
@client.voice_mutexes[@guild_id].unlock
|
221
|
+
start_receive true
|
222
|
+
rescue Protocol::WebSocket::ClosedError => e
|
223
|
+
case e.code
|
224
|
+
when 4014
|
225
|
+
@status = :closed
|
226
|
+
cleanup
|
227
|
+
when 4006
|
228
|
+
@status = :reconnecting
|
229
|
+
@connect_condition = Async::Condition.new
|
230
|
+
@client.voice_mutexes[@guild_id].unlock
|
231
|
+
start_receive false
|
232
|
+
else
|
233
|
+
@status = :closed
|
234
|
+
cleanup
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def handle_voice_connection(message)
|
241
|
+
@client.logger.debug("Voice connection message: #{message}")
|
242
|
+
data = message[:d]
|
243
|
+
# pp data
|
244
|
+
case message[:op]
|
245
|
+
when 8
|
246
|
+
@heartbeat_task = handle_heartbeat(data[:heartbeat_interval])
|
247
|
+
when 2
|
248
|
+
@port, @ip = data[:port], data[:ip]
|
249
|
+
@client.logger.debug("Connecting to voice UDP, #{@ip}:#{@port}")
|
250
|
+
@sockaddr = Socket.pack_sockaddr_in(@port, @ip)
|
251
|
+
@voice_connection = UDPSocket.new
|
252
|
+
@voice_connection.connect(@ip, @port)
|
253
|
+
@ssrc = data[:ssrc]
|
254
|
+
|
255
|
+
@local_ip, @local_port = discover_ip.wait
|
256
|
+
# p @local_ip, @local_port
|
257
|
+
send_connection_message(1, {
|
258
|
+
protocol: "udp",
|
259
|
+
data: {
|
260
|
+
address: @local_ip,
|
261
|
+
port: @local_port,
|
262
|
+
mode: "xsalsa20_poly1305",
|
263
|
+
},
|
264
|
+
})
|
265
|
+
@sequence = 0
|
266
|
+
@timestamp = 0
|
267
|
+
when 4
|
268
|
+
@secret_key = data[:secret_key].pack("C*")
|
269
|
+
@box = RbNaCl::SecretBox.new(@secret_key)
|
270
|
+
@connect_condition.signal
|
271
|
+
@status = :ready
|
272
|
+
when 9
|
273
|
+
@connect_condition.signal
|
274
|
+
@status = :ready
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
def create_header
|
279
|
+
[0x80, 0x78, @sequence, @timestamp, @ssrc].pack("CCnNN").ljust(12, "\0")
|
280
|
+
end
|
281
|
+
|
282
|
+
def encrypt_audio(buf, nonce)
|
283
|
+
@box.box(nonce.ljust(24, "\0"), buf)
|
284
|
+
end
|
285
|
+
|
286
|
+
def discover_ip
|
287
|
+
Async do
|
288
|
+
packet = [
|
289
|
+
1, 70, @ssrc,
|
290
|
+
].pack("S>S>I>").ljust(70, "\0")
|
291
|
+
@voice_connection.send(packet, 0, @sockaddr)
|
292
|
+
recv = @voice_connection.recv(70)
|
293
|
+
ip_start = 4
|
294
|
+
ip_end = recv.index("\0", ip_start)
|
295
|
+
[recv[ip_start...ip_end], recv[-2, 2].unpack1("S>")]
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
def handle_heartbeat(interval)
|
300
|
+
Async do
|
301
|
+
loop do
|
302
|
+
sleep(interval / 1000.0 * 0.9)
|
303
|
+
send_connection_message(3, Time.now.to_i)
|
304
|
+
end
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
def send_connection_message(opcode, data)
|
309
|
+
@connection.write(
|
310
|
+
{
|
311
|
+
op: opcode,
|
312
|
+
d: data,
|
313
|
+
}.to_json
|
314
|
+
)
|
315
|
+
@connection.flush
|
316
|
+
rescue IOError, Errno::EPIPE
|
317
|
+
return if @status == :reconnecting
|
318
|
+
@status = :reconnecting
|
319
|
+
@client.logger.warn("Voice Websocket connection closed")
|
320
|
+
@connection.close
|
321
|
+
@connect_condition = Async::Condition.new
|
322
|
+
start_receive true
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|
data/lib/discorb/voice/error.rb
CHANGED
@@ -1,7 +1,10 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Discorb
|
3
|
+
module Voice
|
4
|
+
#
|
5
|
+
# Error for voice connection.
|
6
|
+
#
|
7
|
+
class VoiceError < StandardError
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|