Skip to content

relatel/librevox

 
 

Repository files navigation

Librevox

A Ruby library for interacting with FreeSWITCH through mod_event_socket, using async I/O.

Documentation: https://relatel.github.io/librevox

Table of Contents

Prerequisites

You should be familiar with mod_event_socket and the differences between inbound and outbound event sockets before getting started.

Requires Ruby 3.0+.

Installation

Add to your Gemfile:

gem "librevox"

Inbound Listener

Subclass Librevox::Listener::Inbound to create an inbound listener. It connects to FreeSWITCH and subscribes to events.

Events

React to events in two ways:

  1. Override on_event, called for every event.
  2. Use event hooks 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
end

Event Filtering

By 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'
end

Note 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.

Outbound Listener

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.

Dialplan

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
end

Applications 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
end

For 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"
end

API Commands

To 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"
end

Starting Listeners

Start a single listener:

Librevox.start MyInbound

With 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
end

Default ports are 8021 for inbound and 8084 for outbound.

Closing Connections

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
end

Command Socket

Librevox::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.close

Configuration

Librevox.options[:log_file]  = "librevox.log"  # default: STDOUT
Librevox.options[:log_level] = Logger::DEBUG    # default: Logger::INFO

Event Socket Protocol

Understanding the outbound event socket protocol is important for working on librevox internals.

Outbound session lifecycle

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

sendmsg and application execution

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

event-lock

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.

Two fibers per connection

Librevox runs two fibers for each connection:

  • Session fiber (run_session) — runs the setup sequence and then session_initiated. Each send_message or application call creates an Async::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.

API Documentation

Applications and commands are documented with YARD. Generate docs with:

yard doc

See Librevox::Applications and Librevox::Commands for the full API reference.

License

MIT. See LICENSE for details.

About

ruby library for interacting with freeswitch

Resources

Stars

Watchers

Forks

Releases

Contributors

Languages