This repository has been archived by the owner on May 31, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 53
/
VoiceConnection.swift
583 lines (440 loc) · 14.5 KB
/
VoiceConnection.swift
1
2
3
4
5
6
7
8
9
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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
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
210
211
212
213
214
215
216
217
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
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
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
//
// VoiceConnection.swift
// Sword
//
// Created by Alejandro Alonso
// Copyright © 2017 Alejandro Alonso. All rights reserved.
//
#if !os(iOS)
import Foundation
import Dispatch
import Sockets
#if os(macOS)
import Starscream
import Sodium
#else
import WebSockets
import SodiumLinux
#endif
/// Voice Connection class that handles connection to voice server
public class VoiceConnection: Gateway, Eventable {
// MARK: Properties
/// Gets current time in milliseconds
var currentTime: Int {
return Int(Date().timeIntervalSince1970 * 1000)
}
/// The VoiceEncoder for this connection
var encoder: Encoder?
/// Used to block encoder process
let encoderSema = DispatchSemaphore(value: 1)
/// Array of endpoint components
let endpoint: [String]
/// URL to connect to WS
var gatewayUrl: String
/// Guild that this voice connection is server
public let guildId: GuildID
/// Completion handler function that calls when voice connection is ready
var handler: (VoiceConnection) -> ()
/// The Heartbeat to send through WS
var heartbeat: Heartbeat?
/// Payload to send over gateway to initialize a voice connection
var identify: String?
/// Whether or not the WS is connected
var isConnected = false
/// Whether or not the voice connection is playing something
public var isPlaying = false
/// Event listeners
public var listeners = [Event: [(Any) -> ()]]()
/// Port number for udp client
var port: Int
/// Secret key to use to encrypt voice data
var secret = [UInt8]()
/// The WS voice connection connects to
var session: WebSocket?
/// Whether or not we need to make a new encoder
var shouldMakeEncoder = true {
willSet {
self.encoderSema.wait()
}
didSet {
self.encoderSema.signal()
}
}
/// SSRC used to encrypt voice
var ssrc: UInt32 = 0
/// Time we started sending voice through udp
var startTime = 0
/// The UDP Client used to send audio through
var udpClient: UDPInternetSocket?
/// The dispatch queue that handles reading audio
var udpReadQueue: DispatchQueue
/// The dispatch queue that handles sending audio
var udpWriteQueue: DispatchQueue
/// The encoder's writePipe to send audio to
var writer: FileHandle? {
return self.encoder?.writer.fileHandleForWriting
}
/// Used in making of rtp header
#if os(macOS)
var sequence = UInt16(arc4random() >> 16)
var timestamp = UInt32(arc4random())
#else
var sequence = UInt16(random() >> 16)
var timestamp = UInt32(random())
#endif
// MARK: Initializer
/**
Creates a VoiceConnection object that handles connecting to voice servers
- parameter endpoint: URL of the voice channel WS needs to connect to
- parameter guildId: Guild we're connecting to
- parameter handler: Completion handler to call after we're ready
*/
init(_ endpoint: String, _ guildId: GuildID, _ handler: @escaping (VoiceConnection) -> ()) {
self.endpoint = endpoint.components(separatedBy: ":")
self.gatewayUrl = "wss://\(self.endpoint[0])"
self.guildId = guildId
self.port = Int(self.endpoint[1])!
self.handler = handler
self.udpReadQueue = DispatchQueue(label: "gg.azoy.sword.voiceConnection.udpRead.\(guildId)")
self.udpWriteQueue = DispatchQueue(label: "gg.azoy.sword.voiceConnection.udpWrite.\(guildId)")
_ = sodium_init()
signal(SIGPIPE, SIG_IGN)
}
/// Called when VoiceConnection needs to free up space
deinit {
self.stop()
}
// MARK: Functions
/**
Makes the Thread sleep to keep a constant audio sending rateLimited
- parameter count: Number in sending sequence of writing data to udp client
*/
func audioSleep(for count: Int) {
let inner = (self.startTime + count * 20) - self.currentTime
let waitTime = Double(20 + inner) / 1000
guard waitTime > 0 else { return }
Thread.sleep(forTimeInterval: waitTime)
}
/// Creates a VoiceEncoder
func createEncoder(volume: Int = 100) {
self.shouldMakeEncoder = false
self.encoder = nil
self.encoder = Encoder(volume: volume)
self.readEncoder(for: 1)
self.shouldMakeEncoder = true
}
/**
Creates a voice packet to send through udp client
- parameter data: Voice Data to send to client
*/
func createPacket(with data: [UInt8]) throws -> [UInt8] {
let header = self.createRTPHeader()
var nonce = header + [UInt8](repeating: 0x00, count: 12)
var buffer = data
#if os(macOS)
let audioSize = Int(crypto_secretbox_MACBYTES) + data.count
#else
let audioSize = 16 + data.count
#endif
let audioData = UnsafeMutablePointer<UInt8>.allocate(capacity: audioSize)
defer {
free(audioData)
}
let encrypted = crypto_secretbox_easy(audioData, &buffer, UInt64(buffer.count), &nonce, &self.secret)
guard encrypted != -1 else {
throw VoiceError.encryptionFail
}
let encryptedAudioData = Array(UnsafeBufferPointer(start: audioData, count: audioSize))
return header + encryptedAudioData
}
/// Creates the RTP Header to use in a packet
func createRTPHeader() -> [UInt8] {
let header = UnsafeMutableRawBufferPointer.allocate(count: 12)
defer {
header.deallocate()
}
header.storeBytes(of: 0x80, as: UInt8.self)
header.storeBytes(of: 0x78, toByteOffset: 1, as: UInt8.self)
header.storeBytes(of: self.sequence.bigEndian, toByteOffset: 2, as: UInt16.self)
header.storeBytes(of: self.timestamp.bigEndian, toByteOffset: 4, as: UInt32.self)
header.storeBytes(of: self.ssrc.bigEndian, toByteOffset: 8, as: UInt32.self)
return Array(header)
}
/**
Decrypts a voice packet from udp client
- parameter data: Raw data to separate to get rtp header and voice data
*/
func decryptPacket(with data: Data) throws -> [UInt8] {
let header = Array(data.prefix(12))
var nonce = header + [UInt8](repeating: 0x00, count: 12)
let audioData = Array(data.dropFirst(12))
#if os(macOS)
let audioSize = audioData.count - Int(crypto_secretbox_MACBYTES)
#else
let audioSize = audioData.count - 16
#endif
let unencryptedAudioData = UnsafeMutablePointer<UInt8>.allocate(capacity: audioSize)
defer {
free(unencryptedAudioData)
}
let unencrypted = crypto_secretbox_open_easy(unencryptedAudioData, audioData, UInt64(data.count - 12), &nonce, &self.secret)
guard unencrypted != -1 else {
throw VoiceError.decryptionFail
}
return Array(UnsafeBufferPointer(start: unencryptedAudioData, count: audioSize))
}
/// Creates a new encoder when old one is done
func doneReading() {
self.encoderSema.wait()
guard self.shouldMakeEncoder else {
self.encoderSema.signal()
return
}
self.encoderSema.signal()
self.createEncoder()
}
/// Used to tell encoder to close the write pipe
public func finish() {
self.encoder?.finish()
}
/// Handles what to do on connect to gateway
func handleConnect() {
guard self.identify != nil else { return }
#if os(macOS)
self.session?.write(string: self.identify!)
#else
try? self.session?.send(self.identify!)
#endif
}
/**
Handles what to do on disconnect from gateway
- parameter code: Error code received from gateway
*/
func handleDisconnect(for code: Int) {
guard CloseOP(rawValue: code) != nil else {
print("[Sword] Voice connection closed with unrecognized response\nCode: \(code)")
return
}
}
/**
Handles all WS events
- parameter payload: Payload that was sent through WS
*/
func handlePayload(_ payload: Payload) {
guard payload.t != nil else {
guard let voiceOP = VoiceOP(rawValue: payload.op) else { return }
guard let data = payload.d as? [String: Any] else {
self.heartbeat?.received = true
return
}
switch voiceOP {
case .ready:
self.heartbeat = Heartbeat(self.session!, "heartbeat.voiceconnection.\(self.guildId)", interval: data["heartbeat_interval"] as! Int, voice: true)
self.heartbeat?.received = true
self.heartbeat?.send()
self.ssrc = data["ssrc"] as! UInt32
self.startUDPSocket(data["port"] as! Int)
case .sessionDescription:
self.secret = data["secret_key"] as! [UInt8]
default:
break
}
return
}
}
/**
Moves the bot to a new channel
- parameter endpoint: New endpoint to connect to
- parameter identify: New identify to send to WS
- parameter handler: New completion handler to call once voice connection is ready
*/
func moveChannels(_ gatewayUrl: String, _ identify: String, _ handler: @escaping (VoiceConnection) -> ()) {
self.gatewayUrl = gatewayUrl
self.identify = identify
self.handler = handler
self.udpReadQueue = DispatchQueue(label: "gg.azoy.sword.voiceConnection.udpRead.\(guildId)")
self.udpWriteQueue = DispatchQueue(label: "gg.azoy.sword.voiceConnection.udpWrite.\(guildId)")
#if os(macOS)
self.session?.disconnect()
#else
try? self.session?.close()
#endif
try? self.udpClient?.close()
self.start()
}
/**
Plays a file
- parameter location: Location of the file to play
*/
public func play(_ location: String, volume: Int = 100) {
guard location.contains(".") else {
print("[Sword] The file you want to play doesn't have an extension.")
return
}
let locationPaths = location.components(separatedBy: ".")
let process = Process()
process.launchPath = "/usr/local/bin/ffmpeg"
process.arguments = [
"-loglevel", "quiet",
"-i", location,
"-f", locationPaths[locationPaths.count - 1],
"-"
]
self.play(process, volume: volume)
}
/**
Gets a process' info and sets its output to encoder's writePipe, then launches it
- parameter process: Audio process to play from
*/
public func play(_ process: Process, volume: Int = 100) {
guard !process.isRunning else {
print("[Sword] The audio process passed to play from has already launched. Don't launch the process.")
return
}
var volume = volume
if volume > 200 {
print("[Sword] The volume you want to use was considered too loud. Using default: 100.")
volume = 100
}
self.createEncoder(volume: volume)
process.standardOutput = self.writer
process.terminationHandler = { [unowned self] _ in
self.finish()
}
process.launch()
self.on(.connectionClose) { [weak process] _ in
guard let process = process else { return }
kill(process.processIdentifier, SIGKILL)
}
}
/**
Plays a youtube video/youtube-dl related sites
- parameter youtube: Youtube structure to play
*/
public func play(_ youtube: Youtube, volume: Int = 100) {
self.play(youtube.process, volume: volume)
}
/**
Reads data from the encoder
- parameter amount: Number in sequence of reading encoder
*/
func readEncoder(for amount: Int) {
self.encoder?.readFromPipe { [weak self] done, data in
guard let this = self, this.isConnected else { return }
this.isPlaying = true
guard !done else {
this.doneReading()
this.isPlaying = false
return
}
if amount == 1 {
this.startTime = this.currentTime
this.setSpeaking(to: true)
}
this.sendPacket(with: data)
this.audioSleep(for: amount)
this.readEncoder(for: amount + 1)
}
}
/// Reads audio data from udp client
func receiveAudio() {
self.udpReadQueue.async { [weak self] in
guard let client = self?.udpClient else { return }
do {
let (data, _) = try client.recvfrom(maxBytes: 4096)
guard let audioData = try self?.decryptPacket(with: Data(bytes: data)) else { return }
self?.emit(.audioData, with: audioData)
}catch {
guard let isConnected = self?.isConnected, !isConnected else { return }
print("[Sword] Unable to read voice data from guild: \(self?.guildId as Any).")
}
self?.receiveAudio()
}
}
/**
Sends a WS event that contains the protocol of audio we're sending, and user IP and Port
- parameter bytes: Raw data to get user's IP and Port from
*/
func selectProtocol(_ bytes: [UInt8]) {
let localIp = String(data: Data(bytes: bytes.dropLast(2)), encoding: .utf8)!.replacingOccurrences(of: "\0", with: "")
let localPort = Int(bytes[68]) + (Int(bytes[69]) << 8)
let payload = Payload(voiceOP: .selectProtocol, data: ["protocol": "udp", "data": ["address": localIp, "port": localPort, "mode": "xsalsa20_poly1305"]]).encode()
#if os(macOS)
self.session?.write(string: payload)
#else
try? self.session?.send(payload)
#endif
if self.encoder != nil {
self.readEncoder(for: 1)
}
self.handler(self)
self.receiveAudio()
}
/**
Sends a voice packet through the udp client
- parameter data: Encrypted audio to send to udp client
*/
func sendPacket(with data: [UInt8]) {
self.udpWriteQueue.async { [unowned self] in
guard data.count <= 320 else { return }
do {
try self.udpClient?.sendto(data: self.createPacket(with: data))
}catch {
guard let isClosed = self.udpClient?.isClosed, isClosed else {
self.stop()
return
}
return
}
self.sequence = self.sequence &+ 1
self.timestamp = self.timestamp &+ 960
}
}
/**
Sets the bot's speaking toggle
- parameter value: Whether or not we want to speak
*/
func setSpeaking(to value: Bool) {
let payload = Payload(voiceOP: .speaking, data: ["speaking": value, "delay": 0]).encode()
#if os(macOS)
self.session?.write(string: payload)
#else
try? self.session?.send(payload)
#endif
}
/**
Creates UDP client to send audio through
- parameter port: Port to use to connect to client
*/
func startUDPSocket(_ port: Int) {
let address = InternetAddress(hostname: self.endpoint[0], port: Port(port))
guard let client = try? UDPInternetSocket(address: address) else {
self.stop()
return
}
self.udpClient = client
do {
try client.sendto(data: [UInt8](repeating: 0x00, count: 70))
let (data, _) = try client.recvfrom(maxBytes: 70)
self.selectProtocol(data)
} catch {
self.stop()
}
}
/// Stops WS, UDP, and Encoder
func stop() {
if self.isConnected {
self.emit(.connectionClose)
}
#if !os(Linux)
self.session?.disconnect()
#else
try? self.session?.close()
#endif
try? self.udpClient?.close()
self.heartbeat = nil
self.isConnected = false
self.encoder = nil
}
}
#endif