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.
classMyInbound < Librevox::Listener::Inbounddefon_event(e)puts"Got event: #{e.content[:event_name]}"endevent:channel_hangupdodo_somethingend# The hook block receives a Response when it takes an argument:event:channel_bridgedo |e|
putse.content[:caller_caller_id_number]enddefdo_something# ...endendBy default, inbound listeners subscribe to all events. Use events to limit which events are received, and filters to filter by header values:
classMyInbound < Librevox::Listener::Inboundevents['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:
classMyOutbound < Librevox::Listener::Outbounddefsession_initiatedanswerdigit=play_and_get_digits"enter-digit.wav","bad-digit.wav"bridge"sofia/gateway/trunk/#{digit}"endendApplications that read input (like play_and_get_digits and read) return the collected value directly.
defsession_initiatedanswerset"foo","bar"multiset"baz"=>"1","qux"=>"2"playback"welcome.wav"hangupendFor apps not yet wrapped by a named helper, call application directly:
application"park"Channel variables are available through session (a hash) and variable:
defsession_initiatedanswernumber=variable(:destination_number)playback"greeting-#{number}.wav"endTo avoid name clashes between applications and commands, commands are accessed through api:
defsession_initiatedanswerapi.statusapi.originate'sofia/user/coltrane',extension: "1234"endStart a single listener:
Librevox.startMyInboundWith connection options:
Librevox.startMyInbound,host: "1.2.3.4",port: 8021,auth: "secret"Start multiple listeners:
Librevox.startdorunMyInboundrunMyOutbound,port: 8084endDefault 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:
classMyOutbound < Librevox::Listener::Outboundevent:channel_hangupdodisconnectendendLibrevox::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: STDOUTLibrevox.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.