mumble-ruby2 1.1.4
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/.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
|