mumble-ruby2 1.1.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.rdoc +119 -0
- data/Rakefile +2 -0
- data/lib/mumble-ruby2.rb +39 -0
- data/lib/mumble-ruby2/audio_player.rb +107 -0
- data/lib/mumble-ruby2/audio_recorder.rb +79 -0
- data/lib/mumble-ruby2/cert_manager.rb +67 -0
- data/lib/mumble-ruby2/channel.rb +48 -0
- data/lib/mumble-ruby2/client.rb +218 -0
- data/lib/mumble-ruby2/connection.rb +85 -0
- data/lib/mumble-ruby2/img_reader.rb +37 -0
- data/lib/mumble-ruby2/messages.rb +361 -0
- data/lib/mumble-ruby2/model.rb +43 -0
- data/lib/mumble-ruby2/mumble.proto +291 -0
- data/lib/mumble-ruby2/packet_data_stream.rb +157 -0
- data/lib/mumble-ruby2/thread_tools.rb +24 -0
- data/lib/mumble-ruby2/user.rb +51 -0
- data/lib/mumble-ruby2/version.rb +3 -0
- data/mumble-ruby2.gemspec +24 -0
- metadata +137 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b0b2f66cdde08f10db6f6af8e9fd08eedf13cf856b0674446c4ea726238c09f4
|
4
|
+
data.tar.gz: 2938bc5085b878c7fa0611074bd9ca3ed1d140210b0a6b95fb5a3dec23f1e272
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 78bd56173cb547f21bf784bd3c16dd8851c415141f100bf4c1f4954c60eda2132fbb43839e1ac15bc199bd838f8c1dd216ef467cbd1898e9a866fc33e977dfa8
|
7
|
+
data.tar.gz: 87df7a958f3b142d8418e0da1d53c3c4b20c865a5d95e35588508e158c38021427d4a14589a745c6f8e1339fa87b021217d84851af47498c636816273911f429
|
data/.gitignore
ADDED
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
mumble_ruby_gem
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-3.0
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Matthew Perry, Philip Mayer
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
mumble-ruby
|
2
|
+
https://github.com/Shadowsith/mumble-ruby2
|
3
|
+
|
4
|
+
== DESCRIPTION:
|
5
|
+
|
6
|
+
Mumble-Ruby is a headless client for the Mumble VOIP application.
|
7
|
+
Mumble-Ruby provides the ability to write scripts and bots which interact with mumble servers through a simple DSL.
|
8
|
+
Mumble-Ruby also has the ability to stream raw audio from a fifo pipe (mpd) to the mumble server.
|
9
|
+
There is huge room for improvement in this library and I am willing to accept all sorts of pull requests so please do.
|
10
|
+
|
11
|
+
== INSTALL:
|
12
|
+
|
13
|
+
[sudo] gem install mumble-ruby2
|
14
|
+
|
15
|
+
== REQUIREMENTS:
|
16
|
+
|
17
|
+
* Ruby >= 2.1.0
|
18
|
+
* OPUS Audio Codec
|
19
|
+
* Murmur server > 1.2.4 -- NOTE: mumble-ruby2 will not be able to stream audio to servers that don't support OPUS anymore. I haven't looked into backwards-compatability with CELT.
|
20
|
+
|
21
|
+
== RECENT CHANGES:
|
22
|
+
|
23
|
+
* Merged changes for proper user/channel objects
|
24
|
+
* Merged changes for recording feature
|
25
|
+
* Added half-broken support for playing files (Something is wrong, wouldn't recommend using it)
|
26
|
+
* Bit of refactoring and renaming
|
27
|
+
|
28
|
+
* Added OPUS support
|
29
|
+
* Added more configuration options
|
30
|
+
* Added image text messages
|
31
|
+
* Added ssl cert auth
|
32
|
+
* Fixed several bugs
|
33
|
+
|
34
|
+
== BASIC USAGE:
|
35
|
+
|
36
|
+
# Configure all clients globally
|
37
|
+
Mumble.configure do |conf|
|
38
|
+
# sample rate of sound (48 khz recommended)
|
39
|
+
conf.sample_rate = 48000
|
40
|
+
|
41
|
+
# bitrate of sound (32 kbit/s recommended)
|
42
|
+
conf.bitrate = 32000
|
43
|
+
|
44
|
+
# directory to store user's ssl certs
|
45
|
+
conf.ssl_cert_opts[:cert_dir] = File.expand_path("./")
|
46
|
+
end
|
47
|
+
|
48
|
+
# Create client instance for your server
|
49
|
+
cli = Mumble::Client.new('localhost') do |conf|
|
50
|
+
conf.username = 'Mumble Bot'
|
51
|
+
conf.password = 'password123'
|
52
|
+
|
53
|
+
# Overwrite global config
|
54
|
+
conf.bitrate = 48000
|
55
|
+
end
|
56
|
+
# => #<Mumble::Client:0x00000003064fe8 @host="localhost", @port=64738, @username="Mumble Bot", @password="password123", @channels={}, @users={}, @callbacks={}>
|
57
|
+
|
58
|
+
# Set up some callbacks for when you recieve text messages
|
59
|
+
# There are callbacks for every Mumble Protocol Message that a client can recieve
|
60
|
+
# For a reference on those, see the linked PDF at the bottom of the README.
|
61
|
+
cli.on_text_message do |msg|
|
62
|
+
puts msg.message
|
63
|
+
end
|
64
|
+
# => [#<Proc:0x0000000346e5f8@(irb):2>]
|
65
|
+
|
66
|
+
# Initiate the connection to the client
|
67
|
+
cli.connect
|
68
|
+
# => #<Thread:0x000000033d7388 run>
|
69
|
+
|
70
|
+
# Mute and Deafen yourself after connecting
|
71
|
+
cli.on_connected do
|
72
|
+
cli.me.mute
|
73
|
+
cli.me.deafen
|
74
|
+
end
|
75
|
+
|
76
|
+
# Join the channel titled "Chillen" (this will return a channel object for that channel)
|
77
|
+
cli.join_channel('Chillen')
|
78
|
+
|
79
|
+
# Get a list of channels
|
80
|
+
cli.channels
|
81
|
+
# Returns a hash of channel_id: Channel objects
|
82
|
+
|
83
|
+
# Join Channel using ID
|
84
|
+
cli.join_channel(0)
|
85
|
+
|
86
|
+
# Join Channel using Channel object
|
87
|
+
cli.join_channel(cli.channels[0])
|
88
|
+
cli.channels[0].join
|
89
|
+
|
90
|
+
# Get a list of users
|
91
|
+
cli.users
|
92
|
+
# Returns a hash of session_id: UserState Messages
|
93
|
+
|
94
|
+
# Text user
|
95
|
+
cli.text_user('perrym5', "Hello there, I'm a robot!")
|
96
|
+
|
97
|
+
# Text an image to a channel
|
98
|
+
cli.text_channel_img('Chillen', '/path/to/image.jpg')
|
99
|
+
|
100
|
+
# Start streaming from a FIFO queue of raw PCM data
|
101
|
+
cli.player.stream_named_pipe('/tmp/mpd.fifo')
|
102
|
+
|
103
|
+
# EXPERIMENTAL: Recording feature
|
104
|
+
cli.recorder.start('/home/matt/record.wav')
|
105
|
+
sleep(2)
|
106
|
+
cli.recorder.stop
|
107
|
+
|
108
|
+
# EXPERIMENTAL: Play wav files
|
109
|
+
cli.player.play_file('/home/matt/record.wav')
|
110
|
+
|
111
|
+
# Safely disconnect
|
112
|
+
cli.disconnect
|
113
|
+
# => nil
|
114
|
+
|
115
|
+
== MUMBLE PROTOCOL:
|
116
|
+
|
117
|
+
The documentation for Mumble's control and voice protocol is a good reference for using this client as all of the callbacks
|
118
|
+
are based on the types of messages the Mumble uses to accomplish its tasks.
|
119
|
+
You can see it here[https://github.com/mumble-voip/mumble-protocol/blob/master/protocol_stack_tcp.rst].
|
data/Rakefile
ADDED
data/lib/mumble-ruby2.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'opus-ruby'
|
2
|
+
require 'active_support/inflector'
|
3
|
+
require 'mumble-ruby2/version'
|
4
|
+
require 'mumble-ruby2/thread_tools'
|
5
|
+
require 'mumble-ruby2/messages'
|
6
|
+
require 'mumble-ruby2/connection'
|
7
|
+
require 'mumble-ruby2/model'
|
8
|
+
require 'mumble-ruby2/user'
|
9
|
+
require 'mumble-ruby2/channel'
|
10
|
+
require 'mumble-ruby2/client'
|
11
|
+
require 'mumble-ruby2/audio_player'
|
12
|
+
require 'mumble-ruby2/packet_data_stream'
|
13
|
+
require 'mumble-ruby2/img_reader'
|
14
|
+
require 'mumble-ruby2/cert_manager'
|
15
|
+
require 'mumble-ruby2/audio_recorder'
|
16
|
+
require 'hashie'
|
17
|
+
|
18
|
+
module Mumble
|
19
|
+
DEFAULTS = {
|
20
|
+
sample_rate: 48000,
|
21
|
+
bitrate: 32000,
|
22
|
+
ssl_cert_opts: {
|
23
|
+
cert_dir: File.expand_path("./"),
|
24
|
+
country_code: "US",
|
25
|
+
organization: "github.com",
|
26
|
+
organization_unit: "Engineering"
|
27
|
+
}
|
28
|
+
}
|
29
|
+
|
30
|
+
def self.configuration
|
31
|
+
@configuration ||= Hashie::Mash.new(DEFAULTS)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.configure
|
35
|
+
yield(configuration) if block_given?
|
36
|
+
end
|
37
|
+
|
38
|
+
Thread.abort_on_exception = true
|
39
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'wavefile'
|
2
|
+
|
3
|
+
module Mumble
|
4
|
+
class AudioPlayer
|
5
|
+
include ThreadTools
|
6
|
+
COMPRESSED_SIZE = 960
|
7
|
+
|
8
|
+
def initialize(type, connection, sample_rate, bitrate)
|
9
|
+
@packet_header = (type << 5).chr
|
10
|
+
@conn = connection
|
11
|
+
@pds = PacketDataStream.new
|
12
|
+
@queue = Queue.new
|
13
|
+
@wav_format = WaveFile::Format.new :mono, :pcm_16, sample_rate
|
14
|
+
|
15
|
+
create_encoder sample_rate, bitrate
|
16
|
+
end
|
17
|
+
|
18
|
+
def volume
|
19
|
+
@volume ||= 100
|
20
|
+
end
|
21
|
+
|
22
|
+
def volume=(volume)
|
23
|
+
@volume = volume
|
24
|
+
end
|
25
|
+
|
26
|
+
def playing?
|
27
|
+
@playing ||= false
|
28
|
+
end
|
29
|
+
|
30
|
+
def play_file(file)
|
31
|
+
unless playing?
|
32
|
+
@file = WaveFile::Reader.new(file, @wav_format)
|
33
|
+
Thread.new { bounded_produce }
|
34
|
+
@playing = true
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def stream_named_pipe(pipe)
|
39
|
+
unless playing?
|
40
|
+
@file = File.open(pipe, 'rb')
|
41
|
+
spawn_threads :produce, :consume
|
42
|
+
@playing = true
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def stop
|
47
|
+
if playing?
|
48
|
+
kill_threads
|
49
|
+
@encoder.reset
|
50
|
+
@file.close unless @file.closed?
|
51
|
+
@playing = false
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
def create_encoder(sample_rate, bitrate)
|
57
|
+
@encoder = Opus::Encoder.new sample_rate, sample_rate / 100, 1
|
58
|
+
@encoder.vbr_rate = 0 # CBR
|
59
|
+
@encoder.bitrate = bitrate
|
60
|
+
end
|
61
|
+
|
62
|
+
def change_volume(pcm_data)
|
63
|
+
pcm_data.unpack('s*').map { |s| s * (volume / 100.0) }.pack('s*')
|
64
|
+
end
|
65
|
+
|
66
|
+
def bounded_produce
|
67
|
+
frame_count = 0
|
68
|
+
start_time = Time.now.to_f
|
69
|
+
@file.each_buffer(@encoder.frame_size) do |buffer|
|
70
|
+
encode_sample buffer.samples.pack('s*')
|
71
|
+
consume
|
72
|
+
frame_count += 1
|
73
|
+
wait_time = start_time - Time.now.to_f + frame_count * 0.01
|
74
|
+
sleep(wait_time) if wait_time > 0
|
75
|
+
end
|
76
|
+
|
77
|
+
stop
|
78
|
+
end
|
79
|
+
|
80
|
+
def produce
|
81
|
+
encode_sample @file.read(@encoder.frame_size * 2)
|
82
|
+
end
|
83
|
+
|
84
|
+
def encode_sample(sample)
|
85
|
+
pcm_data = change_volume sample
|
86
|
+
@queue << @encoder.encode(pcm_data, COMPRESSED_SIZE)
|
87
|
+
end
|
88
|
+
|
89
|
+
def consume
|
90
|
+
@seq ||= 0
|
91
|
+
@seq %= 1000000 # Keep sequence number reasonable for long runs
|
92
|
+
|
93
|
+
@pds.rewind
|
94
|
+
@seq += 1
|
95
|
+
@pds.put_int @seq
|
96
|
+
|
97
|
+
frame = @queue.pop
|
98
|
+
@pds.put_int frame.size
|
99
|
+
@pds.append_block frame
|
100
|
+
|
101
|
+
size = @pds.size
|
102
|
+
@pds.rewind
|
103
|
+
data = [@packet_header, @pds.get_block(size)].flatten.join
|
104
|
+
@conn.send_udp_packet data
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'wavefile'
|
2
|
+
require 'thread'
|
3
|
+
|
4
|
+
module Mumble
|
5
|
+
class AudioRecorder
|
6
|
+
include ThreadTools
|
7
|
+
|
8
|
+
def initialize(client, sample_rate)
|
9
|
+
@client = client
|
10
|
+
@wav_format = WaveFile::Format.new(:mono, :pcm_16, sample_rate)
|
11
|
+
@pds = PacketDataStream.new
|
12
|
+
@pds_lock = Mutex.new
|
13
|
+
|
14
|
+
@decoders = Hash.new do |h, k|
|
15
|
+
h[k] = Opus::Decoder.new sample_rate, sample_rate / 100, 1
|
16
|
+
end
|
17
|
+
|
18
|
+
@queues = Hash.new do |h, k|
|
19
|
+
h[k] = Queue.new
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def recording?
|
24
|
+
@recording ||= false
|
25
|
+
end
|
26
|
+
|
27
|
+
def start(file)
|
28
|
+
unless recording?
|
29
|
+
@file = WaveFile::Writer.new(file, @wav_format)
|
30
|
+
@callback = @client.on_udp_tunnel { |msg| process_udp_tunnel msg }
|
31
|
+
spawn_thread :write_audio
|
32
|
+
@recording = true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def stop
|
37
|
+
if recording?
|
38
|
+
@client.remove_callback :udp_tunnel, @callback
|
39
|
+
kill_threads
|
40
|
+
@decoders.values.each &:destroy
|
41
|
+
@decoders.clear
|
42
|
+
@queues.clear
|
43
|
+
@file.close
|
44
|
+
@recording = false
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
def process_udp_tunnel(message)
|
50
|
+
@pds_lock.synchronize do
|
51
|
+
@pds.rewind
|
52
|
+
@pds.append_block message.packet[1..-1]
|
53
|
+
|
54
|
+
@pds.rewind
|
55
|
+
source = @pds.get_int
|
56
|
+
seq = @pds.get_int
|
57
|
+
len = @pds.get_int
|
58
|
+
opus = @pds.get_block len
|
59
|
+
|
60
|
+
@queues[source] << @decoders[source].decode(opus.join)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# TODO: Better audio stream merge with normalization
|
65
|
+
def write_audio
|
66
|
+
pcms = @queues.values
|
67
|
+
.reject { |q| q.empty? } # Remove empty queues
|
68
|
+
.map { |q| q.pop.unpack 's*' } # Grab the top element of each queue and expand
|
69
|
+
|
70
|
+
head, *tail = pcms
|
71
|
+
if head
|
72
|
+
samples = head.zip(*tail)
|
73
|
+
.map { |pcms| pcms.reduce(:+) / pcms.size } # Average together all the columns of the matrix (merge audio streams)
|
74
|
+
.flatten # Flatten the resulting 1d matrix
|
75
|
+
@file.write WaveFile::Buffer.new(samples, @wav_format)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
module Mumble
|
5
|
+
class CertManager
|
6
|
+
attr_reader :key, :cert
|
7
|
+
|
8
|
+
CERT_STRING = "/C=%s/O=%s/OU=%s/CN=%s"
|
9
|
+
|
10
|
+
def initialize(username, opts)
|
11
|
+
@cert_dir = File.join(opts[:cert_dir], "#{username.downcase}_cert")
|
12
|
+
@username = username
|
13
|
+
@opts = opts
|
14
|
+
|
15
|
+
FileUtils.mkdir_p @cert_dir
|
16
|
+
setup_key
|
17
|
+
setup_cert
|
18
|
+
end
|
19
|
+
|
20
|
+
[:private_key, :public_key, :cert].each do |sym|
|
21
|
+
define_method "#{sym}_path" do
|
22
|
+
File.join(@cert_dir, "#{sym}.pem")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
def setup_key
|
28
|
+
if File.exists?(private_key_path)
|
29
|
+
@key ||= OpenSSL::PKey::RSA.new File.read(private_key_path)
|
30
|
+
else
|
31
|
+
@key ||= OpenSSL::PKey::RSA.new 2048
|
32
|
+
File.write private_key_path, key.to_pem
|
33
|
+
File.write public_key_path, key.public_key.to_pem
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def setup_cert
|
38
|
+
if File.exists?(cert_path)
|
39
|
+
@cert ||= OpenSSL::X509::Certificate.new File.read(cert_path)
|
40
|
+
else
|
41
|
+
@cert ||= OpenSSL::X509::Certificate.new
|
42
|
+
|
43
|
+
subject = CERT_STRING % [@opts[:country_code], @opts[:organization], @opts[:organization_unit], @username]
|
44
|
+
|
45
|
+
cert.subject = cert.issuer = OpenSSL::X509::Name.parse(subject)
|
46
|
+
cert.not_before = Time.now
|
47
|
+
cert.not_after = Time.new + 365 * 24 * 60 * 60 * 5
|
48
|
+
cert.public_key = key.public_key
|
49
|
+
cert.serial = rand(65535) + 1
|
50
|
+
cert.version = 2
|
51
|
+
|
52
|
+
ef = OpenSSL::X509::ExtensionFactory.new
|
53
|
+
ef.subject_certificate = cert
|
54
|
+
ef.issuer_certificate = cert
|
55
|
+
|
56
|
+
cert.add_extension(ef.create_extension("basicConstraints", "CA:TRUE", true))
|
57
|
+
cert.add_extension(ef.create_extension("keyUsage", "keyCertSign, cRLSign", true))
|
58
|
+
cert.add_extension(ef.create_extension("subjectKeyIdentifier", "hash", false))
|
59
|
+
cert.add_extension(ef.create_extension("authorityKeyIdentifier", "keyid:always", false))
|
60
|
+
|
61
|
+
cert.sign key, OpenSSL::Digest::SHA256.new
|
62
|
+
|
63
|
+
File.write cert_path, cert.to_pem
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|