A Ruby library for interacting with FreeSWITCH through mod_event_socket, using async I/O.
Documentation: https://relatel.github.io/librevox
- Prerequisites
- Installation
- Inbound Listener
- Outbound Listener
- Starting Listeners
- Closing Connections
- Command Socket
- Configuration
- Event Socket Protocol
- API Documentation
- License
You should be familiar with mod_event_socket and the differences between inbound and outbound event sockets before getting started.
Requires Ruby 3.0+.
Add to your Gemfile:
gem "librevox"Subclass Librevox::Listener::Inbound to create an inbound listener. It connects to FreeSWITCH and subscribes to events.
React to events in two ways:
- Override
on_event, called for every event. - Use
eventhooks for specific event names.
class MyInbound < Librevox::Listener::Inbound
def on_event(e)
puts "Got event: #{e.content[:event_name]}"
end
event :channel_hangup do
do_something
end
# The hook block receives a Response when it takes an argument:
event :channel_bridge do |e|
puts e.content[:caller_caller_id_number]
end
def do_something
# ...
end
endBy default, inbound listeners subscribe to all events. Use events to limit which events are received, and filters to filter by header values:
class MyInbound < Librevox::Listener::Inbound
events ['CHANNEL_EXECUTE', 'CUSTOM foo']
filters 'Caller-Context' => ['default', 'example'],
'Caller-Privacy-Hide-Name' => 'no'
endNote on CUSTOM events: FreeSWITCH custom events have a subclass name (e.g. CUSTOM conference::maintenance). You must include both the event name and subclass — events ['CUSTOM conference::maintenance']. Using just events ['CUSTOM'] will not match any custom events.
Subclass Librevox::Listener::Outbound to create an outbound listener. FreeSWITCH connects to it when a call hits a socket application in the dialplan.
Outbound listeners have the same event functionality as inbound, but scoped to the session.
When FreeSWITCH connects, session_initiated is called. Build your dialplan here.
Each application call blocks until FreeSWITCH signals completion (CHANNEL_EXECUTE_COMPLETE), so applications execute sequentially:
class MyOutbound < Librevox::Listener::Outbound
def session_initiated
answer
digit = play_and_get_digits "enter-digit.wav", "bad-digit.wav"
bridge "sofia/gateway/trunk/#{digit}"
end
endApplications that read input (like play_and_get_digits and read) return the collected value directly.
def session_initiated
answer
set "foo", "bar"
multiset "baz" => "1", "qux" => "2"
playback "welcome.wav"
hangup
endFor apps not yet wrapped by a named helper, call application directly:
application "park"Channel variables are available through session (a hash) and variable:
def session_initiated
answer
number = variable(:destination_number)
playback "greeting-#{number}.wav"
endTo avoid name clashes between applications and commands, commands are accessed through api:
def session_initiated
answer
api.status
api.originate 'sofia/user/coltrane', extension: "1234"
endStart a single listener:
Librevox.start MyInboundWith connection options:
Librevox.start MyInbound, host: "1.2.3.4", port: 8021, auth: "secret"Start multiple listeners:
Librevox.start do
run MyInbound
run MyOutbound, port: 8084
endDefault ports are 8021 for inbound and 8084 for outbound.
After a session ends (e.g. the caller hangs up), the outbound socket connection to FreeSWITCH remains open for post-session events. Close it manually when done to avoid lingering sessions:
class MyOutbound < Librevox::Listener::Outbound
event :channel_hangup do
disconnect
end
endLibrevox::CommandSocket connects to the FreeSWITCH management console for one-off commands:
require "librevox/command_socket"
socket = Librevox::CommandSocket.new(server: "127.0.0.1", port: 8021, auth: "ClueCon")
socket.originate 'sofia/user/coltrane', extension: "1234"
#=> #<Librevox::Protocol::Response ...>
socket.status
#=> #<Librevox::Protocol::Response ...>
socket.closeLibrevox.options[:log_file] = "librevox.log" # default: STDOUT
Librevox.options[:log_level] = Logger::DEBUG # default: Logger::INFOUnderstanding the outbound event socket protocol is important for working on librevox internals.
When FreeSWITCH hits a socket application in the dialplan, it connects to the
outbound listener. The listener sends three setup commands before any
application logic runs:
Listener → FS: connect
FS → Listener: (channel data — becomes @session)
Listener → FS: myevents
FS → Listener: command/reply +OK
Listener → FS: linger
FS → Listener: command/reply +OK → triggers session_initiated
When an application (e.g. answer, playback, bridge) is executed via
sendmsg, FreeSWITCH always sends the command/reply +OK immediately — it is
an acknowledgement that the sendmsg was received, not that the application
finished. Application completion is signalled by a CHANNEL_EXECUTE_COMPLETE
event:
Listener → FS: sendmsg <uuid>
call-command: execute
execute-app-name: playback
execute-app-arg: welcome.wav
event-lock: true
FS → Listener: command/reply +OK ← immediate ack
FS → Listener: CHANNEL_EXECUTE event ← app started
...app is running...
FS → Listener: CHANNEL_EXECUTE_COMPLETE event ← app finished
The event-lock: true header serializes application execution on the
channel. It does not change what is sent back on the socket.
Without event-lock, if multiple sendmsg commands are pipelined, FreeSWITCH
may dequeue and start executing the next application before the current one
finishes. With event-lock: true, FreeSWITCH sets an internal flag
(CF_EVENT_LOCK) on the channel that prevents the next queued sendmsg from
being processed until the current application completes.
Librevox runs two fibers for each connection:
- Session fiber (
run_session) — runs the setup sequence and thensession_initiated. Eachsend_messageorapplicationcall creates anAsync::Promise, pushes it onto an array, and blocks the fiber until the promise is resolved. - Read fiber (
each_message) — reads messages from the socket and resolves promises in FIFO order, waking the session fiber.
No mutex is needed — Ruby's cooperative fiber scheduling guarantees that the
promise push happens before the I/O yield point (the socket write), so
interleaving from concurrent event-hook fibers is safe. When a connection
drops, pending promises are rejected with ConnectionError.
Applications and commands are documented with YARD. Generate docs with:
yard doc
See Librevox::Applications and Librevox::Commands for the full API reference.
MIT. See LICENSE for details.