ConfBridge Conference Rooms + Monitoring for ViciDial/Asterisk
Replace MeetMe with ConfBridge, Add Admin Controls, and Monitor Conference Activity
| Difficulty | Intermediate |
| Time to Complete | 2-3 hours |
| Prerequisites | Asterisk 13+ (16+ recommended), ViciDial (optional), Linux server, MariaDB/MySQL |
| Tested On | Asterisk 16.30.0, Asterisk 20.18.2, ViciBox 10, openSUSE 15.x, CentOS 7 |
Table of Contents
- Introduction
- MeetMe vs ConfBridge
- Architecture Overview
- Prerequisites
- Step 1: Verify ConfBridge Module
- Step 2: Configure Bridge Profiles
- Step 3: Configure User Profiles
- Step 4: Configure Admin Profiles and Menus
- Step 5: Dialplan — User and Admin Entry
- Step 6: ViciDial ConfBridge Integration
- Step 7: Conference Monitor Script
- Step 8: Admin Controls — Mute, Kick, Lock
- Step 9: Database Logging Table
- Step 10: Cron and Automation
- Step 11: Testing and Verification
- Step 12: Full Migration Script
- Troubleshooting
- Performance Tuning
- Security Considerations
- What's Next
Introduction
Every Asterisk-based call center uses conference bridges. When an agent takes a call in ViciDial, both the agent and the caller are placed into a MeetMe or ConfBridge conference room. This is how the system mixes audio, enables call monitoring (whisper/barge/listen), and handles transfers.
The problem: MeetMe is the legacy conferencing application in Asterisk. It requires DAHDI timing hardware or kernel modules, produces the infamous "water drop" sound when participants join, and has been deprecated since Asterisk 13. On newer Asterisk versions (16+), MeetMe is still available but is no longer actively developed.
ConfBridge is the modern replacement. It uses kernel timing (no DAHDI required), supports per-user profiles with granular control over sounds and permissions, and handles large conferences more efficiently. For ViciDial deployments, the key advantage is eliminating the bloop/water-drop sound that customers hear when being connected to an agent -- a dead giveaway that the call is going through a call center system.
This tutorial covers three things:
- Setting up ConfBridge with proper profiles for agents, customers, admins, and monitors
- Creating dialplan extensions for numbered conference rooms with admin access
- Building a PHP monitoring script that detects and handles the ViciDial "double-call" conference bug where multiple callers end up in the same agent conference
Everything here comes from production ViciDial deployments running Asterisk 16 and 20, handling hundreds of concurrent calls daily.
MeetMe vs ConfBridge
Before diving into configuration, here is a direct comparison:
| Feature | MeetMe | ConfBridge |
|---|---|---|
| DAHDI Required | Yes -- needs dahdi_dummy or hardware timing |
No -- uses kernel timerfd |
| Join/Leave Sounds | Limited control (global only) | Per-user and per-bridge sound control |
| User Profiles | None -- all users treated equally | Named profiles with individual settings |
| Admin Controls | Basic (lock/mute via DTMF) | Full admin profiles with custom DTMF menus |
| Jitterbuffer | Not available | Per-user jitterbuffer option |
| Silence Detection | Not available | dsp_drop_silence for performance |
| Video Support | No | Yes (follow talker, SFU, etc.) |
| Status | Deprecated (Asterisk 13+) | Actively maintained |
| ViciDial Support | Default (all versions) | Supported via patches (ViciDial 2.14+) |
| Water Drop Sound | Always plays on join | Fully controllable per user profile |
Bottom line: If you are running Asterisk 13 or newer, you should be using ConfBridge. If you are on Asterisk 11 (like some older ViciDial installs on CentOS 7), you are stuck with MeetMe -- ConfBridge was introduced in Asterisk 10 but was not production-ready until Asterisk 13.
Architecture Overview
ConfBridge Architecture
=====================
+-----------+ +-------------+ +------------+
| Agent | --> | Asterisk | <-- | Customer |
| Phone | | ConfBridge | | (Inbound) |
+-----------+ +------+------+ +------------+
|
+------------+-------------+
| | |
+-----+----+ +----+-----+ +-----+-----+
| Bridge | | User | | DTMF |
| Profile | | Profiles | | Menus |
+----------+ +----------+ +-----------+
max_members admin=yes toggle_mute
sample_rate quiet=yes kick_last
sounds startmuted lock_conf
Conference Rooms:
+--------+--------+--------+--------+--------+
| 9000 | 9001 | 9002 | ... | 9009 |
| (user) | (user) | (user) | | (user) |
+--------+--------+--------+--------+--------+
| 9100 |
| (admin |
| room |
| 9000) |
+--------+
ViciDial Agent Conferences (if integrated):
+----------+----------+----------+----------+
| 9600000 | 9600001 | 9600002 | ... |
| (cust) | (cust) | (cust) | |
+----------+----------+----------+----------+
| 29600000 | 29600001 | ... | Prefix |
| (agent) | (agent) | | Routing |
+----------+----------+----------+----------+
Monitor Script (PHP, runs every 5-10s via cron):
+-------------+ +-----------+ +----------+
| Cron Job | --> | PHP | --> | Asterisk |
| (every 5s) | | Monitor | | CLI |
+-------------+ +-----+-----+ +----------+
|
+-----+------+
| Database |
| Logging |
+------------+
The system has three layers:
- ConfBridge Configuration (
confbridge.conf) -- bridge profiles, user profiles, and DTMF menus that define how conferences behave - Dialplan Extensions (
extensions.conforcustomexte.conf) -- numbered extensions that map dial codes to conference rooms with specific profiles - Monitoring Script (PHP) -- a cron-driven script that queries Asterisk for active conferences, detects anomalies (like multiple callers in a single-agent conference), and takes corrective action
Prerequisites
- Asterisk 13+ (16+ recommended, 20+ ideal)
- Linux server with root access
- PHP 7.4+ with MySQLi extension (for the monitoring script)
- MariaDB/MySQL (for logging and ViciDial integration)
- Basic Asterisk knowledge -- you should know what a dialplan context is and how to reload Asterisk
Check your Asterisk version:
asterisk -rx "core show version"
Verify the ConfBridge module is available:
asterisk -rx "module show like confbridge"
You should see app_confbridge.so in the output. If not, you need to compile Asterisk with ConfBridge support (covered in Step 1).
Step 1: Verify ConfBridge Module
ConfBridge is built into Asterisk by default on versions 13+. Verify it is loaded:
asterisk -rx "module show like confbridge"
Expected output:
Module Description Use Count Status
app_confbridge.so Conference Bridge Application 0 Running
1 modules loaded
If the module is not present, load it:
asterisk -rx "module load app_confbridge.so"
If it fails to load, you may need to recompile Asterisk with ConfBridge support:
cd /usr/src/asterisk-*/
make menuselect
# Navigate to Applications, enable app_confbridge
make && make install
Timing Module
ConfBridge needs a timing source. Unlike MeetMe, it does not require DAHDI. The preferred timing module is res_timing_timerfd (kernel-based, zero overhead):
asterisk -rx "module show like timing"
Expected output:
Module Description Use Count Status
res_timing_timerfd.so /dev/timerfd Timing Interface 1 Running
If multiple timing modules are loaded, force timerfd by adding to /etc/asterisk/modules.conf:
; Force timerfd timing (best performance, no DAHDI needed)
noload => res_timing_dahdi.so
noload => res_timing_kqueue.so
noload => res_timing_pthread.so
Note: If you are running MeetMe and ConfBridge simultaneously (during migration), keep DAHDI timing loaded. MeetMe requires it. Once you have fully migrated to ConfBridge, you can unload DAHDI timing.
Step 2: Configure Bridge Profiles
Bridge profiles define the behavior of the conference room itself -- how many members, what sample rate, what sounds to play. Create or edit /etc/asterisk/confbridge.conf:
; =============================================================================
; /etc/asterisk/confbridge.conf
; ConfBridge Configuration
; =============================================================================
[general]
; Reserved for future use
; -----------------------------------------------------------------------------
; DEFAULT PROFILES (applied when no profile is specified)
; -----------------------------------------------------------------------------
[default_bridge]
type=bridge
max_members=50
internal_sample_rate=8000
mixing_interval=20
video_mode=none
[default_user]
type=user
quiet=yes
announce_user_count=no
announce_only_user=no
dtmf_passthrough=yes
jitterbuffer=yes
Now add custom bridge profiles. These give you separate configurations for different use cases:
; -----------------------------------------------------------------------------
; BRIDGE PROFILES
; -----------------------------------------------------------------------------
; Standard conference bridge (for ad-hoc conference rooms)
[standard_bridge]
type=bridge
max_members=50
internal_sample_rate=8000
mixing_interval=20
video_mode=none
language=en
; Agent conference bridge (for ViciDial agent conferences)
; All sounds silenced to prevent customer hearing join/leave notifications
[agent_bridge]
type=bridge
max_members=10
record_conference=no
internal_sample_rate=8000
mixing_interval=20
video_mode=none
sound_join=enter
sound_leave=leave
sound_has_joined=sip-silence
sound_has_left=sip-silence
sound_kicked=sip-silence
sound_muted=sip-silence
sound_unmuted=sip-silence
sound_only_person=confbridge-only-participant
sound_only_one=sip-silence
sound_there_are=sip-silence
sound_other_in_party=sip-silence
sound_begin=sip-silence
sound_wait_for_leader=sip-silence
sound_leader_has_left=sip-silence
sound_get_pin=sip-silence
sound_invalid_pin=sip-silence
sound_locked=sip-silence
sound_locked_now=sip-silence
sound_unlocked_now=sip-silence
sound_error_menu=sip-silence
sound_participants_muted=sip-silence
; Recorded conference bridge (all conversations recorded)
[recorded_bridge]
type=bridge
max_members=20
record_conference=yes
record_file_timestamp=yes
internal_sample_rate=8000
mixing_interval=20
video_mode=none
language=en
Key Bridge Profile Options Explained
| Option | Default | Description |
|---|---|---|
max_members |
unlimited | Maximum participants. Admin users bypass this limit. |
internal_sample_rate |
auto | Audio mixing sample rate. Use 8000 for telephony (saves CPU). |
mixing_interval |
20 | How often audio is mixed (ms). 20ms = low latency. 40ms = lower CPU. |
record_conference |
no | Record the entire conference to a WAV file. |
video_mode |
none | Video distribution mode. Use none for audio-only. |
sound_join |
conf-join | Sound file played when someone joins. Set to sip-silence to disable. |
sound_leave |
conf-leave | Sound file played when someone leaves. |
Production tip: The
sip-silencesound file is a zero-length audio file included with Asterisk. Use it to effectively disable any sound without generating errors about missing files.
Step 3: Configure User Profiles
User profiles define the behavior of individual participants -- whether they are admins, whether they start muted, whether they hear join sounds.
Add these to /etc/asterisk/confbridge.conf:
; -----------------------------------------------------------------------------
; USER PROFILES
; -----------------------------------------------------------------------------
; Standard user (for ad-hoc conference rooms)
[standard_user]
type=user
admin=no
quiet=no
startmuted=no
dtmf_passthrough=yes
jitterbuffer=yes
announce_user_count=no
announce_only_user=no
; Agent user (ViciDial agent channel)
[agent_user]
type=user
admin=no
quiet=no
startmuted=no
marked=yes
dtmf_passthrough=yes
hear_own_join_sound=yes
dsp_drop_silence=yes
jitterbuffer=yes
; Customer user (external caller -- NO water drop sound)
[customer_user]
type=user
admin=no
quiet=no
startmuted=no
marked=yes
dtmf_passthrough=yes
hear_own_join_sound=no
dsp_drop_silence=yes
jitterbuffer=yes
; Admin user (supervisor access to any conference)
[admin_user]
type=user
admin=yes
quiet=no
startmuted=no
marked=yes
dtmf_passthrough=yes
jitterbuffer=yes
; Monitor user (listen-only, completely silent)
[monitor_user]
type=user
admin=no
quiet=yes
startmuted=yes
marked=no
dtmf_passthrough=no
dsp_drop_silence=yes
; Barge user (supervisor barging into a call -- can speak)
[barge_user]
type=user
admin=no
quiet=no
startmuted=no
marked=no
dtmf_passthrough=yes
dsp_drop_silence=yes
; Recording user (for call recording channels)
[recording_user]
type=user
admin=no
quiet=yes
startmuted=yes
marked=no
dtmf_passthrough=no
dsp_drop_silence=yes
Key User Profile Options Explained
| Option | Default | Description |
|---|---|---|
admin |
no | Admin users can use admin DTMF actions (kick, lock, mute all). |
quiet |
no | Suppress all join/leave sounds for this user. |
startmuted |
no | User joins the conference muted. |
marked |
no | Marked users are "important" -- others can wait for them. |
hear_own_join_sound |
yes | Set to no so the customer does not hear the join beep. |
dsp_drop_silence |
no | Drop silence from this user's audio stream. Reduces CPU and background noise. |
dtmf_passthrough |
no | Pass DTMF tones through the conference (needed for IVR navigation). |
jitterbuffer |
no | Apply a jitterbuffer to this user's audio. Adds slight delay but smooths audio. |
wait_marked |
no | User must wait for a marked user before hearing conference audio. |
The "water drop" fix: The key to eliminating the join sound that customers hear is
hear_own_join_sound=noon the customer user profile, combined withquiet=noon the bridge (so agents still hear the join if desired). This is the single biggest reason ViciDial deployments migrate from MeetMe to ConfBridge.
Step 4: Configure Admin Profiles and Menus
DTMF menus allow conference participants to control the conference by pressing keys. You can create separate menus for regular users and admins.
Add these to /etc/asterisk/confbridge.conf:
; -----------------------------------------------------------------------------
; DTMF MENUS
; -----------------------------------------------------------------------------
; Basic user menu
[user_menu]
type=menu
*=playback_and_continue(conf-usermenu)
1=toggle_mute
*1=toggle_mute
4=decrease_listening_volume
6=increase_listening_volume
7=decrease_talking_volume
9=increase_talking_volume
0=leave_conference
*0=leave_conference
; Admin menu (superset of user menu with admin controls)
[admin_menu]
type=menu
*=playback_and_continue(conf-adminmenu)
1=toggle_mute
*1=toggle_mute
2=admin_toggle_conference_lock
*2=admin_toggle_conference_lock
3=admin_kick_last
*3=admin_kick_last
4=decrease_listening_volume
6=increase_listening_volume
7=decrease_talking_volume
8=admin_toggle_mute_participants
*8=admin_toggle_mute_participants
9=increase_talking_volume
0=leave_conference
*0=leave_conference
#=participant_count
DTMF Menu Actions Reference
| Action | Description | Admin Only? |
|---|---|---|
toggle_mute |
Mute/unmute yourself | No |
admin_toggle_conference_lock |
Lock/unlock the conference (no new joins) | Yes |
admin_kick_last |
Kick the last person who joined | Yes |
admin_toggle_mute_participants |
Mute/unmute all non-admin participants | Yes |
leave_conference |
Leave the conference and continue dialplan | No |
participant_count |
Hear how many participants are in the conference | No |
increase_listening_volume |
Increase what you hear | No |
decrease_listening_volume |
Decrease what you hear | No |
increase_talking_volume |
Increase your microphone volume | No |
decrease_talking_volume |
Decrease your microphone volume | No |
dialplan_exec(ctx,ext,pri) |
Jump to dialplan and return | No |
playback(<file>) |
Play a sound file | No |
Step 5: Dialplan -- User and Admin Entry
Now create the dialplan extensions that let people dial into conference rooms. Add these to your dialplan -- either /etc/asterisk/extensions.conf (under a suitable context) or /etc/asterisk/customexte.conf if you are on ViciDial.
Basic Numbered Conference Rooms (9000-9009)
; =============================================================================
; ConfBridge Conference Room Extensions
; Add to /etc/asterisk/extensions.conf or /etc/asterisk/customexte.conf
; =============================================================================
; --- Standard conference rooms (dial 9000-9009) ---
; Anyone dialing these extensions joins as a standard user.
exten => 9000,1,Answer()
same => n,ConfBridge(9000,standard_bridge,standard_user,user_menu)
same => n,Hangup()
; Pattern match for rooms 9001-9009
exten => _900[1-9],1,Answer()
same => n,ConfBridge(${EXTEN},standard_bridge,standard_user,user_menu)
same => n,Hangup()
Admin Entry Extensions (9100-9109)
; --- Admin conference entry (dial 9100-9109) ---
; Maps to rooms 9000-9009 but with admin privileges.
; Dial 9100 = admin in room 9000, 9101 = admin in room 9001, etc.
exten => 9100,1,Answer()
same => n,ConfBridge(9000,standard_bridge,admin_user,admin_menu)
same => n,Hangup()
exten => _910[1-9],1,Answer()
same => n,Set(ROOM=${MATH(${EXTEN}-100,int)})
same => n,ConfBridge(${ROOM},standard_bridge,admin_user,admin_menu)
same => n,Hangup()
PIN-Protected Conference Rooms (9200-9209)
; --- PIN-protected conference rooms (dial 9200-9209) ---
; Prompts for a PIN before joining. PIN is set in the user profile.
[pin_user]
type=user
admin=no
quiet=no
startmuted=no
dtmf_passthrough=yes
pin=YOUR_PIN_HERE
; Extensions (add to dialplan)
exten => _920[0-9],1,Answer()
same => n,Set(ROOM=${MATH(${EXTEN}-200+9000,int)})
same => n,ConfBridge(${ROOM},standard_bridge,pin_user,user_menu)
same => n,Hangup()
Recorded Conference Rooms (9300-9309)
; --- Recorded conference rooms (dial 9300-9309) ---
; Conference audio is recorded to Asterisk's monitor directory.
exten => _930[0-9],1,Answer()
same => n,Set(ROOM=${MATH(${EXTEN}-300+9000,int)})
same => n,Set(CONFBRIDGE(bridge,record_file)=/var/spool/asterisk/monitor/conf-${ROOM}-${STRFTIME(${EPOCH},,%Y%m%d-%H%M%S)})
same => n,ConfBridge(${ROOM},recorded_bridge,standard_user,user_menu)
same => n,Hangup()
Monitor/Listen-Only Entry (9400)
; --- Monitor entry (dial 9400+room) ---
; Supervisor can listen to a conference silently.
; Example: Dial 9400 to silently listen to room 9000.
exten => _940[0-9],1,Answer()
same => n,Set(ROOM=${MATH(${EXTEN}-400+9000,int)})
same => n,ConfBridge(${ROOM},standard_bridge,monitor_user)
same => n,Hangup()
After adding extensions, reload the dialplan:
asterisk -rx "dialplan reload"
Verify the extensions are loaded:
asterisk -rx "dialplan show 9000@default"
asterisk -rx "dialplan show 9100@default"
Step 6: ViciDial ConfBridge Integration
If you are running ViciDial, the conferencing system is deeply integrated. ViciDial uses conferences for every agent call -- the agent and caller are bridged together in a conference room. Migrating from MeetMe to ConfBridge requires specific extensions with prefix-based routing.
ViciDial Conference Extensions
ViciDial uses conference numbers in the range 9600000-9600299 (configurable). Different prefixes route the channel to different user profiles:
; =============================================================================
; ViciDial ConfBridge Extensions
; Add to /etc/asterisk/extensions.conf
; =============================================================================
; Customer channel into agent conference (no prefix)
exten => _9600XXX,1,Answer()
exten => _9600XXX,n,Playback(sip-silence)
exten => _9600XXX,n,ConfBridge(${EXTEN},agent_bridge,customer_user)
exten => _9600XXX,n,Hangup()
; Agent channel into conference (prefix 2)
exten => _29600XXX,1,Answer()
exten => _29600XXX,n,Playback(sip-silence)
exten => _29600XXX,n,ConfBridge(${EXTEN:1},agent_bridge,agent_user)
exten => _29600XXX,n,Hangup()
; Admin/supervisor into conference (prefix 3)
exten => _39600XXX,1,Answer()
exten => _39600XXX,n,Playback(sip-silence)
exten => _39600XXX,n,ConfBridge(${EXTEN:1},agent_bridge,admin_user)
exten => _39600XXX,n,Hangup()
; Monitor/listen-only into conference (prefix 4)
exten => _49600XXX,1,Answer()
exten => _49600XXX,n,Playback(sip-silence)
exten => _49600XXX,n,ConfBridge(${EXTEN:1},agent_bridge,monitor_user)
exten => _49600XXX,n,Hangup()
; Recording channel into conference (prefix 5)
exten => _59600XXX,1,Answer()
exten => _59600XXX,n,Playback(sip-silence)
exten => _59600XXX,n,ConfBridge(${EXTEN:1},agent_bridge,recording_user)
exten => _59600XXX,n,Hangup()
; Barge into conference (prefix 6)
exten => _69600XXX,1,Answer()
exten => _69600XXX,n,Playback(sip-silence)
exten => _69600XXX,n,ConfBridge(${EXTEN:1},agent_bridge,barge_user)
exten => _69600XXX,n,Hangup()
; DTMF injection into conference (prefix 7)
exten => _79600XXX,1,Answer()
exten => _79600XXX,n,Playback(sip-silence)
exten => _79600XXX,n,ConfBridge(${EXTEN:1},agent_bridge,recording_user)
exten => _79600XXX,n,Hangup()
; Audio playback into conference (prefix 8)
exten => _89600XXX,1,Answer()
exten => _89600XXX,n,Playback(sip-silence)
exten => _89600XXX,n,ConfBridge(${EXTEN:1},agent_bridge,recording_user)
exten => _89600XXX,n,Hangup()
; Kick all from conference (prefix 9)
exten => _99600XXX,1,ConfKick(${EXTEN:1},all)
exten => _99600XXX,2,Hangup()
exten => _55559600XXX,1,ConfKick(${EXTEN:4},all)
exten => _55559600XXX,2,Hangup()
; RINGALL agent channel (prefix 1)
exten => _19600XXX,1,Answer()
exten => _19600XXX,n,Playback(sip-silence)
exten => _19600XXX,n,ConfBridge(${EXTEN:1},agent_bridge,agent_user)
exten => _19600XXX,n,Hangup()
ViciDial Database Configuration
ViciDial needs conference entries in the vicidial_confbridges table. Insert 300 conference room entries for your server:
-- Create conference entries for ViciDial
-- Replace YOUR_SERVER_IP with your Asterisk server's IP
INSERT INTO vicidial_confbridges (conf_exten, server_ip, extension)
SELECT 9600000 + seq, 'YOUR_SERVER_IP', ''
FROM (
SELECT @row := @row + 1 AS seq
FROM information_schema.columns a,
information_schema.columns b,
(SELECT @row := -1) r
LIMIT 300
) numbers;
-- Verify
SELECT COUNT(*) FROM vicidial_confbridges
WHERE server_ip = 'YOUR_SERVER_IP';
ViciDial Admin GUI Changes
After configuring the files, change the conferencing engine in the ViciDial admin panel:
- Navigate to Admin > Servers
- Click on your server entry
- Change Conferencing Engine from
MEETMEtoCONFBRIDGE - Set Rebuild conf files to
Y - Click SUBMIT
ViciDial keepalive Configuration
Add the C flag to VARactive_keepalives in /etc/astguiclient.conf:
; Before:
VARactive_keepalives => 123456
; After (add C for ConfBridge screen updates):
VARactive_keepalives => 123456C
AMI Manager User
Add a manager user for the conference monitoring cron job in /etc/asterisk/manager.conf:
[confcron]
secret = YOUR_AMI_SECRET
read = command,reporting
write = command,reporting
eventfilter=Event: Meetme
eventfilter=Event: Confbridge
Reload the manager:
asterisk -rx "manager reload"
Step 7: Conference Monitor Script
This is a PHP script that runs via cron every 5-10 seconds. It monitors all active ViciDial agent conferences and detects cases where multiple inbound calls end up in the same conference room -- a known ViciDial bug that can cause callers to hear each other.
Create /usr/local/bin/conference_monitor.php:
#!/usr/bin/php
<?php
/**
* ViciDial Conference Monitor Script
*
* Purpose: Detects and handles cases where multiple inbound calls
* end up in the same agent conference room.
*
* This addresses a known ViciDial bug where race conditions can
* cause two callers to be placed into the same agent conference,
* letting them hear each other. References:
* - https://vicidial.org/VICIDIALforum/viewtopic.php?t=23656
* - https://vicidial.org/VICIDIALforum/viewtopic.php?f=4&t=34791
*
* Run via cron every 5-10 seconds:
* * * * * /usr/bin/php /usr/local/bin/conference_monitor.php >> /var/log/conference_monitor.log 2>&1
* * * * * sleep 5 && /usr/bin/php /usr/local/bin/conference_monitor.php >> /var/log/conference_monitor.log 2>&1
* * * * * sleep 10 && /usr/bin/php /usr/local/bin/conference_monitor.php >> /var/log/conference_monitor.log 2>&1
*
* Configuration options:
* action = 'log' - Only log the issue (safe, default)
* action = 'hangup_newest' - Kick the newest external caller
* action = 'hangup_oldest' - Kick the oldest external caller
* action = 'alert' - Send email alert
*/
// =============================================================================
// CONFIGURATION
// =============================================================================
$config = array(
// Database connection
'db_host' => 'localhost',
'db_user' => 'YOUR_DB_USER',
'db_pass' => 'YOUR_DB_PASS',
'db_name' => 'asterisk',
// Conference limits
'max_callers_per_conference' => 2, // Agent + 1 caller = 2 max
// Action to take when too many callers detected
// Options: 'log', 'hangup_newest', 'hangup_oldest', 'alert'
'action' => 'log',
// Logging
'log_file' => '/var/log/conference_monitor.log',
// Alerting
'alert_email' => '', // Set email address for alerts
// Debug mode (verbose logging)
'debug' => true
);
// =============================================================================
// FUNCTIONS
// =============================================================================
/**
* Log a message with timestamp
*/
function logMsg($msg, $config) {
$timestamp = date('Y-m-d H:i:s');
$logLine = "[$timestamp] $msg\n";
if ($config['debug']) {
echo $logLine;
}
file_put_contents($config['log_file'], $logLine, FILE_APPEND);
}
/**
* Get members of a conference using Asterisk CLI
* Works with both MeetMe and ConfBridge
*/
function getConferenceMembers($confNum) {
// Try ConfBridge first
$cmd = "asterisk -rx 'confbridge list $confNum' 2>/dev/null";
$output = shell_exec($cmd);
if (empty($output) || strpos($output, 'No active') !== false) {
// Fall back to MeetMe
$cmd = "asterisk -rx 'meetme list $confNum' 2>/dev/null";
$output = shell_exec($cmd);
}
if (empty($output) || strpos($output, 'No active') !== false) {
return array();
}
$members = array();
$lines = explode("\n", trim($output));
foreach ($lines as $line) {
// Parse ConfBridge list output
// Format varies by version, common pattern:
// "Channel: SIP/1001-00000001 Flags: A Talking: No"
if (preg_match('/^(\S+)\s+/', $line, $matches)) {
if (strpos($matches[1], 'Channel') !== false) continue;
if (strpos($matches[1], '===') !== false) continue;
$members[] = array(
'channel' => $matches[1],
'type' => (strpos($line, 'Admin') !== false) ? 'admin' : 'user'
);
}
// Parse MeetMe list output
// Format: "User #: X <channel> (admin|user) <flags>"
if (preg_match('/User #:\s*(\d+)\s+(\S+)\s+\(([^)]+)\)/', $line, $matches)) {
$members[] = array(
'user_num' => $matches[1],
'channel' => $matches[2],
'type' => $matches[3]
);
}
}
return $members;
}
/**
* Kick a user from a conference (MeetMe)
*/
function kickFromMeetMe($confNum, $userNum) {
$cmd = "asterisk -rx 'meetme kick $confNum $userNum' 2>/dev/null";
return shell_exec($cmd);
}
/**
* Kick a channel from a conference (ConfBridge)
*/
function kickFromConfBridge($confNum, $channel) {
$cmd = "asterisk -rx 'confbridge kick $confNum $channel' 2>/dev/null";
return shell_exec($cmd);
}
/**
* Hang up a channel
*/
function hangupChannel($channel) {
$cmd = "asterisk -rx 'channel request hangup $channel' 2>/dev/null";
return shell_exec($cmd);
}
// =============================================================================
// MAIN MONITORING LOGIC
// =============================================================================
// Database connection
$mysqli = new mysqli(
$config['db_host'],
$config['db_user'],
$config['db_pass'],
$config['db_name']
);
if ($mysqli->connect_error) {
die("Database connection failed: " . $mysqli->connect_error . "\n");
}
logMsg("=== Conference Monitor Started ===", $config);
// Query all active agent conferences
$sql = "SELECT vla.user, vla.conf_exten, vla.status, vla.lead_id,
vla.callerid, vla.channel,
vc.extension AS conf_extension
FROM vicidial_live_agents vla
LEFT JOIN vicidial_conferences vc
ON vla.conf_exten = vc.conf_exten
WHERE vla.status IN ('INCALL', 'QUEUE')
AND vla.conf_exten IS NOT NULL
AND vla.conf_exten != ''";
$result = $mysqli->query($sql);
if (!$result) {
logMsg("Query error: " . $mysqli->error, $config);
exit(1);
}
$issues_found = 0;
while ($row = $result->fetch_assoc()) {
$confNum = $row['conf_exten'];
$agentUser = $row['user'];
$agentChan = $row['channel'];
// Get current members of this conference
$members = getConferenceMembers($confNum);
$memberCount = count($members);
if ($config['debug']) {
logMsg("Conference $confNum (Agent: $agentUser): $memberCount members", $config);
}
// Check for too many members
if ($memberCount > $config['max_callers_per_conference']) {
$issues_found++;
// Build member list for logging
$memberList = array();
foreach ($members as $m) {
$memberList[] = $m['channel'] . " (" . $m['type'] . ")";
}
$msg = "ALERT: Conference $confNum has $memberCount members "
. "(max: {$config['max_callers_per_conference']}). "
. "Members: " . implode(", ", $memberList);
logMsg($msg, $config);
// Identify external callers (not Local channels or agent extensions)
$externalCallers = array();
foreach ($members as $m) {
if (strpos($m['channel'], 'Local/') === false &&
strpos($m['channel'], 'SIP/' . $agentUser) === false &&
strpos($m['channel'], 'IAX2/') === false) {
$externalCallers[] = $m;
}
}
logMsg("Found " . count($externalCallers) . " external callers in conference $confNum", $config);
// Take action based on configuration
if (count($externalCallers) > 1) {
switch ($config['action']) {
case 'hangup_newest':
$newest = end($externalCallers);
logMsg("ACTION: Kicking newest caller: {$newest['channel']}", $config);
if (isset($newest['user_num'])) {
kickFromMeetMe($confNum, $newest['user_num']);
} else {
kickFromConfBridge($confNum, $newest['channel']);
}
break;
case 'hangup_oldest':
$oldest = reset($externalCallers);
logMsg("ACTION: Kicking oldest caller: {$oldest['channel']}", $config);
if (isset($oldest['user_num'])) {
kickFromMeetMe($confNum, $oldest['user_num']);
} else {
kickFromConfBridge($confNum, $oldest['channel']);
}
break;
case 'alert':
if (!empty($config['alert_email'])) {
$subject = "Conference Alert - Multiple Callers in $confNum";
$body = "Conference $confNum has multiple callers:\n\n"
. implode("\n", $memberList) . "\n\n"
. "Agent: $agentUser\n"
. "Time: " . date('Y-m-d H:i:s');
mail($config['alert_email'], $subject, $body);
logMsg("ACTION: Alert sent to {$config['alert_email']}", $config);
}
break;
case 'log':
default:
logMsg("ACTION: Logged only (no automatic remediation)", $config);
break;
}
// Log to database for tracking
$logSql = "INSERT INTO conference_monitor_log
(conf_exten, agent_user, member_count,
members_json, action_taken, created_at)
VALUES (?, ?, ?, ?, ?, NOW())";
$stmt = $mysqli->prepare($logSql);
if ($stmt) {
$membersJson = json_encode($members);
$action = $config['action'];
$stmt->bind_param('ssiss',
$confNum, $agentUser, $memberCount,
$membersJson, $action
);
$stmt->execute();
$stmt->close();
}
}
}
}
if ($issues_found > 0) {
logMsg("=== Complete: $issues_found issues found ===", $config);
} else {
if ($config['debug']) {
logMsg("=== Complete: No issues ===", $config);
}
}
$mysqli->close();
?>
Make it executable:
chmod +x /usr/local/bin/conference_monitor.php
Step 8: Admin Controls -- Mute, Kick, Lock
Beyond DTMF menus, you can control conferences from the Asterisk CLI or via AMI (Asterisk Manager Interface). This is useful for building web-based admin panels.
CLI Commands
# List all active conferences
asterisk -rx "confbridge list"
# List members of a specific conference
asterisk -rx "confbridge list 9000"
# Kick a specific channel from a conference
asterisk -rx "confbridge kick 9000 SIP/1001-00000001"
# Kick ALL members from a conference
asterisk -rx "confbridge kick 9000 all"
# Mute a specific channel
asterisk -rx "confbridge mute 9000 SIP/1001-00000001"
# Unmute a specific channel
asterisk -rx "confbridge unmute 9000 SIP/1001-00000001"
# Lock a conference (no new members can join)
asterisk -rx "confbridge lock 9000"
# Unlock a conference
asterisk -rx "confbridge unlock 9000"
# Start recording a conference
asterisk -rx "confbridge record start 9000 /var/spool/asterisk/monitor/conf-9000.wav"
# Stop recording
asterisk -rx "confbridge record stop 9000"
AMI Actions
For web interfaces or automation, use AMI:
Action: ConfbridgeList
Conference: 9000
Action: ConfbridgeKick
Conference: 9000
Channel: SIP/1001-00000001
Action: ConfbridgeMute
Conference: 9000
Channel: SIP/1001-00000001
Action: ConfbridgeUnmute
Conference: 9000
Channel: SIP/1001-00000001
Action: ConfbridgeLock
Conference: 9000
Action: ConfbridgeUnlock
Conference: 9000
Action: ConfbridgeStartRecord
Conference: 9000
Action: ConfbridgeStopRecord
Conference: 9000
PHP Admin Control Function
Here is a reusable PHP function for conference administration via the Asterisk CLI:
<?php
/**
* Conference administration helper functions
* Uses Asterisk CLI commands via shell_exec
*/
class ConferenceAdmin {
/**
* List all active conferences
* @return array Conference numbers and member counts
*/
public static function listConferences() {
$output = shell_exec("asterisk -rx 'confbridge list' 2>/dev/null");
$conferences = array();
if (empty($output) || strpos($output, 'No active') !== false) {
return $conferences;
}
$lines = explode("\n", trim($output));
foreach ($lines as $line) {
if (preg_match('/^(\d+)\s+.*?(\d+)\s+(Locked|Unlocked)/', $line, $m)) {
$conferences[] = array(
'conference' => $m[1],
'members' => (int)$m[2],
'locked' => ($m[3] === 'Locked')
);
}
}
return $conferences;
}
/**
* List members of a specific conference
* @param string $confNum Conference number
* @return array Member details
*/
public static function listMembers($confNum) {
$output = shell_exec("asterisk -rx 'confbridge list $confNum' 2>/dev/null");
$members = array();
if (empty($output)) return $members;
$lines = explode("\n", trim($output));
foreach ($lines as $line) {
// Skip header lines
if (strpos($line, 'Channel') !== false) continue;
if (strpos($line, '===') !== false) continue;
if (empty(trim($line))) continue;
$parts = preg_split('/\s+/', trim($line));
if (count($parts) >= 1 && strpos($parts[0], '/') !== false) {
$members[] = array(
'channel' => $parts[0],
'flags' => isset($parts[1]) ? $parts[1] : '',
'talking' => (strpos($line, 'Talking') !== false),
'muted' => (strpos($line, 'Muted') !== false),
'admin' => (strpos($line, 'Admin') !== false),
);
}
}
return $members;
}
/**
* Kick a channel from a conference
*/
public static function kick($confNum, $channel) {
return shell_exec("asterisk -rx 'confbridge kick $confNum $channel' 2>/dev/null");
}
/**
* Kick all members from a conference
*/
public static function kickAll($confNum) {
return shell_exec("asterisk -rx 'confbridge kick $confNum all' 2>/dev/null");
}
/**
* Mute a channel in a conference
*/
public static function mute($confNum, $channel) {
return shell_exec("asterisk -rx 'confbridge mute $confNum $channel' 2>/dev/null");
}
/**
* Unmute a channel in a conference
*/
public static function unmute($confNum, $channel) {
return shell_exec("asterisk -rx 'confbridge unmute $confNum $channel' 2>/dev/null");
}
/**
* Lock a conference (prevent new members)
*/
public static function lock($confNum) {
return shell_exec("asterisk -rx 'confbridge lock $confNum' 2>/dev/null");
}
/**
* Unlock a conference
*/
public static function unlock($confNum) {
return shell_exec("asterisk -rx 'confbridge unlock $confNum' 2>/dev/null");
}
/**
* Start recording a conference
*/
public static function startRecording($confNum, $filename = null) {
if ($filename === null) {
$filename = "/var/spool/asterisk/monitor/conf-{$confNum}-" . date('Ymd-His') . ".wav";
}
return shell_exec("asterisk -rx 'confbridge record start $confNum $filename' 2>/dev/null");
}
/**
* Stop recording a conference
*/
public static function stopRecording($confNum) {
return shell_exec("asterisk -rx 'confbridge record stop $confNum' 2>/dev/null");
}
}
// Example usage:
// $conferences = ConferenceAdmin::listConferences();
// $members = ConferenceAdmin::listMembers('9000');
// ConferenceAdmin::mute('9000', 'SIP/1001-00000001');
// ConferenceAdmin::kick('9000', 'SIP/1001-00000001');
?>
Step 9: Database Logging Table
Create a table to track conference monitoring events:
CREATE TABLE IF NOT EXISTS conference_monitor_log (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
conf_exten VARCHAR(20) NOT NULL,
agent_user VARCHAR(50) NOT NULL,
member_count INT NOT NULL DEFAULT 0,
members_json TEXT,
action_taken VARCHAR(50) NOT NULL DEFAULT 'log',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_conf_exten (conf_exten),
INDEX idx_agent_user (agent_user),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Useful Queries
-- Recent issues (last 24 hours)
SELECT conf_exten, agent_user, member_count, action_taken, created_at
FROM conference_monitor_log
WHERE created_at >= NOW() - INTERVAL 24 HOUR
ORDER BY created_at DESC;
-- Agents with most conference issues
SELECT agent_user, COUNT(*) AS issue_count
FROM conference_monitor_log
WHERE created_at >= NOW() - INTERVAL 7 DAY
GROUP BY agent_user
ORDER BY issue_count DESC
LIMIT 10;
-- Hourly issue distribution (find peak times)
SELECT HOUR(created_at) AS hour, COUNT(*) AS issues
FROM conference_monitor_log
WHERE created_at >= NOW() - INTERVAL 30 DAY
GROUP BY HOUR(created_at)
ORDER BY hour;
-- Cleanup old entries (keep 90 days)
DELETE FROM conference_monitor_log
WHERE created_at < NOW() - INTERVAL 90 DAY;
Step 10: Cron and Automation
Conference Monitor Cron
The monitor script should run every 5-10 seconds. Since cron only supports minute-level scheduling, use the sleep trick:
# Add to crontab (crontab -e)
# Run conference monitor every 5 seconds
* * * * * /usr/bin/php /usr/local/bin/conference_monitor.php >> /var/log/conference_monitor.log 2>&1
* * * * * sleep 5 && /usr/bin/php /usr/local/bin/conference_monitor.php >> /var/log/conference_monitor.log 2>&1
* * * * * sleep 10 && /usr/bin/php /usr/local/bin/conference_monitor.php >> /var/log/conference_monitor.log 2>&1
* * * * * sleep 15 && /usr/bin/php /usr/local/bin/conference_monitor.php >> /var/log/conference_monitor.log 2>&1
* * * * * sleep 20 && /usr/bin/php /usr/local/bin/conference_monitor.php >> /var/log/conference_monitor.log 2>&1
* * * * * sleep 25 && /usr/bin/php /usr/local/bin/conference_monitor.php >> /var/log/conference_monitor.log 2>&1
* * * * * sleep 30 && /usr/bin/php /usr/local/bin/conference_monitor.php >> /var/log/conference_monitor.log 2>&1
* * * * * sleep 35 && /usr/bin/php /usr/local/bin/conference_monitor.php >> /var/log/conference_monitor.log 2>&1
* * * * * sleep 40 && /usr/bin/php /usr/local/bin/conference_monitor.php >> /var/log/conference_monitor.log 2>&1
* * * * * sleep 45 && /usr/bin/php /usr/local/bin/conference_monitor.php >> /var/log/conference_monitor.log 2>&1
* * * * * sleep 50 && /usr/bin/php /usr/local/bin/conference_monitor.php >> /var/log/conference_monitor.log 2>&1
* * * * * sleep 55 && /usr/bin/php /usr/local/bin/conference_monitor.php >> /var/log/conference_monitor.log 2>&1
Note: Running every 5 seconds is aggressive. For most deployments, every 10 seconds (6 cron lines) is sufficient. The script is lightweight -- it runs one SQL query and a few CLI commands, then exits.
Log Rotation
Add logrotate for the monitor log. Create /etc/logrotate.d/conference_monitor:
/var/log/conference_monitor.log {
daily
rotate 14
compress
missingok
notifempty
create 0644 root root
}
Database Cleanup Cron
# Clean up monitor logs older than 90 days (daily at 3 AM)
0 3 * * * mysql -u YOUR_DB_USER -pYOUR_DB_PASS asterisk -e "DELETE FROM conference_monitor_log WHERE created_at < NOW() - INTERVAL 90 DAY;"
Step 11: Testing and Verification
Test 1: Verify ConfBridge Profiles
# Show all bridge profiles
asterisk -rx "confbridge show profile bridges"
# Show all user profiles
asterisk -rx "confbridge show profile users"
# Show detailed settings for a specific profile
asterisk -rx "confbridge show profile bridge standard_bridge"
asterisk -rx "confbridge show profile user admin_user"
# Show all DTMF menus
asterisk -rx "confbridge show menus"
Test 2: Basic Conference Room
- Register two SIP phones (softphones like Zoiper or Linphone work fine)
- From Phone A, dial
9000-- you should enter conference room 9000 - From Phone B, dial
9000-- you should join the same room - Verify both parties can hear each other
- Test DTMF: Press
1to toggle mute, press0to leave
Test 3: Admin Controls
- From Phone A, dial
9000(join as regular user) - From Phone B, dial
9100(join as admin to room 9000) - As admin (Phone B), press
2to lock the conference - Try joining from a third phone -- it should be rejected
- As admin, press
2again to unlock - As admin, press
3to kick the last person who joined - As admin, press
8to mute all non-admin participants
Test 4: Monitor/Listen-Only
- From Phone A and Phone B, dial
9000 - From Phone C, dial
9400(monitor room 9000) - Verify Phone C can hear A and B but A and B cannot hear C
- Verify Phone C is muted (talking should not be heard by others)
Test 5: CLI Conference Management
# While a conference is active:
# List active conferences
asterisk -rx "confbridge list"
# List members
asterisk -rx "confbridge list 9000"
# Mute a participant
asterisk -rx "confbridge mute 9000 SIP/1001-00000001"
# Kick a participant
asterisk -rx "confbridge kick 9000 SIP/1001-00000001"
# Kick everyone
asterisk -rx "confbridge kick 9000 all"
Test 6: Monitor Script
Run the monitor script manually to verify it works:
/usr/bin/php /usr/local/bin/conference_monitor.php
Expected output (no issues):
[2026-03-13 10:30:00] === Conference Monitor Started ===
[2026-03-13 10:30:00] Conference 9600042 (Agent: 1001): 2 members
[2026-03-13 10:30:00] === Complete: No issues ===
Step 12: Full Migration Script
For a complete MeetMe-to-ConfBridge migration on a ViciDial server, here is a phased installation script. This is a reference -- adapt it to your environment:
#!/bin/bash
#
# ConfBridge Migration Script for ViciDial
# Migrates from MeetMe to ConfBridge
#
# Usage: ./migrate-confbridge.sh [--phase N] [--skip-backup]
#
set -e
# =============================================================================
# CONFIGURATION - EDIT THESE
# =============================================================================
SERVER_IP="YOUR_SERVER_IP"
DB_USER="YOUR_DB_USER"
DB_PASS="YOUR_DB_PASS"
DB_NAME="asterisk"
DATE_STAMP=$(date +%Y%m%d_%H%M%S)
LOG_FILE="/root/confbridge_migration_${DATE_STAMP}.log"
ASTERISK_CONF="/etc/asterisk"
ASTGUICLIENT_DIR="/usr/share/astguiclient"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log() { echo -e "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"; }
info() { echo -e "${GREEN}[INFO]${NC} $1" | tee -a "$LOG_FILE"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1" | tee -a "$LOG_FILE"; }
error() { echo -e "${RED}[ERROR]${NC} $1" | tee -a "$LOG_FILE"; }
# =============================================================================
# PHASE 1: BACKUP
# =============================================================================
phase1_backup() {
log "=== PHASE 1: BACKUP ==="
info "Backing up Asterisk config..."
cp -r "$ASTERISK_CONF" "${ASTERISK_CONF}.bak.${DATE_STAMP}"
info "Backing up astguiclient..."
cp -r "$ASTGUICLIENT_DIR" "${ASTGUICLIENT_DIR}.bak.${DATE_STAMP}"
info "Backups complete."
}
# =============================================================================
# PHASE 2: VERIFY PREREQUISITES
# =============================================================================
phase2_verify() {
log "=== PHASE 2: VERIFY ==="
# Check Asterisk version
local version=$(asterisk -rx "core show version" 2>/dev/null | grep -oP '\d+' | head -1)
if [ "$version" -lt 13 ]; then
error "Asterisk version $version is too old. Need 13+."
exit 1
fi
info "Asterisk major version: $version"
# Check ConfBridge module
if ! asterisk -rx "module show like confbridge" 2>/dev/null | grep -q "app_confbridge"; then
warn "ConfBridge module not loaded. Attempting to load..."
asterisk -rx "module load app_confbridge.so"
fi
info "ConfBridge module loaded."
# Check timing
asterisk -rx "module show like timing" 2>/dev/null | tee -a "$LOG_FILE"
info "Timing modules checked."
}
# =============================================================================
# PHASE 3: CONFIGURE CONFBRIDGE
# =============================================================================
phase3_configure() {
log "=== PHASE 3: CONFIGURE ==="
# Force timerfd timing
if ! grep -q "res_timing_timerfd" "${ASTERISK_CONF}/modules.conf" 2>/dev/null; then
cat >> "${ASTERISK_CONF}/modules.conf" << 'MODEOF'
; ConfBridge timing - prefer timerfd (no DAHDI needed)
noload => res_timing_kqueue.so
noload => res_timing_pthread.so
MODEOF
info "Timing config added to modules.conf"
fi
# Create ConfBridge profiles
info "Writing confbridge.conf profiles..."
# Check if custom profiles already exist
if grep -q "agent_bridge" "${ASTERISK_CONF}/confbridge.conf" 2>/dev/null; then
info "Custom profiles already exist in confbridge.conf"
else
cat >> "${ASTERISK_CONF}/confbridge.conf" << 'CBEOF'
; === Custom ConfBridge Profiles ===
[agent_bridge]
type=bridge
max_members=10
record_conference=no
internal_sample_rate=8000
mixing_interval=20
video_mode=none
sound_join=sip-silence
sound_leave=sip-silence
sound_has_joined=sip-silence
sound_has_left=sip-silence
sound_only_person=sip-silence
sound_only_one=sip-silence
[agent_user]
type=user
admin=no
quiet=no
startmuted=no
marked=yes
dtmf_passthrough=yes
dsp_drop_silence=yes
[customer_user]
type=user
admin=no
quiet=no
startmuted=no
marked=yes
dtmf_passthrough=yes
hear_own_join_sound=no
dsp_drop_silence=yes
[admin_user]
type=user
admin=yes
quiet=yes
dtmf_passthrough=yes
jitterbuffer=yes
[monitor_user]
type=user
admin=no
quiet=yes
startmuted=yes
marked=no
dtmf_passthrough=no
[admin_menu]
type=menu
1=toggle_mute
*1=toggle_mute
2=admin_toggle_conference_lock
3=admin_kick_last
8=admin_toggle_mute_participants
0=leave_conference
CBEOF
info "ConfBridge profiles added."
fi
}
# =============================================================================
# PHASE 4: DATABASE
# =============================================================================
phase4_database() {
log "=== PHASE 4: DATABASE ==="
# Check if vicidial_confbridges table exists
local table_exists=$(mysql -u "$DB_USER" -p"$DB_PASS" -N -e \
"SELECT COUNT(*) FROM information_schema.tables
WHERE table_schema='$DB_NAME' AND table_name='vicidial_confbridges'" 2>/dev/null)
if [ "$table_exists" = "0" ]; then
warn "vicidial_confbridges table does not exist. Your ViciDial version may not support ConfBridge."
warn "You need ViciDial 2.14+ with ConfBridge patches."
return 1
fi
# Count existing entries
local count=$(mysql -u "$DB_USER" -p"$DB_PASS" -N -e \
"SELECT COUNT(*) FROM vicidial_confbridges WHERE server_ip='$SERVER_IP'" "$DB_NAME" 2>/dev/null)
if [ "$count" -gt 0 ]; then
info "Found $count existing ConfBridge entries for $SERVER_IP"
else
info "Inserting 300 conference entries..."
mysql -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" << EOSQL
INSERT INTO vicidial_confbridges (conf_exten, server_ip, extension)
SELECT 9600000 + seq, '$SERVER_IP', ''
FROM (
SELECT @row := @row + 1 AS seq
FROM information_schema.columns a,
information_schema.columns b,
(SELECT @row := -1) r
LIMIT 300
) numbers;
EOSQL
count=$(mysql -u "$DB_USER" -p"$DB_PASS" -N -e \
"SELECT COUNT(*) FROM vicidial_confbridges WHERE server_ip='$SERVER_IP'" "$DB_NAME")
info "Created $count conference entries."
fi
# Create monitor log table
info "Creating conference_monitor_log table..."
mysql -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" << 'EOSQL'
CREATE TABLE IF NOT EXISTS conference_monitor_log (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
conf_exten VARCHAR(20) NOT NULL,
agent_user VARCHAR(50) NOT NULL,
member_count INT NOT NULL DEFAULT 0,
members_json TEXT,
action_taken VARCHAR(50) NOT NULL DEFAULT 'log',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_conf_exten (conf_exten),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
EOSQL
info "Database setup complete."
}
# =============================================================================
# PHASE 5: RELOAD AND VERIFY
# =============================================================================
phase5_reload() {
log "=== PHASE 5: RELOAD AND VERIFY ==="
info "Reloading Asterisk modules..."
asterisk -rx "module reload app_confbridge.so"
asterisk -rx "dialplan reload"
sleep 2
info "Verifying bridge profiles..."
asterisk -rx "confbridge show profile bridges" | tee -a "$LOG_FILE"
info "Verifying user profiles..."
asterisk -rx "confbridge show profile users" | tee -a "$LOG_FILE"
info "Verifying timing..."
asterisk -rx "module show like timing" | tee -a "$LOG_FILE"
log "=== MIGRATION COMPLETE ==="
echo ""
echo "Next steps:"
echo " 1. In ViciDial Admin > Servers, change Conferencing Engine to CONFBRIDGE"
echo " 2. Set 'Rebuild conf files' to Y and click SUBMIT"
echo " 3. Add 'C' to VARactive_keepalives in /etc/astguiclient.conf"
echo " 4. Test with a real call"
echo ""
echo "Log file: $LOG_FILE"
}
# =============================================================================
# MAIN
# =============================================================================
START_PHASE=${1:-1}
if [ "$1" = "--phase" ]; then START_PHASE=$2; fi
[ "$START_PHASE" -le 1 ] && phase1_backup
[ "$START_PHASE" -le 2 ] && phase2_verify
[ "$START_PHASE" -le 3 ] && phase3_configure
[ "$START_PHASE" -le 4 ] && phase4_database
[ "$START_PHASE" -le 5 ] && phase5_reload
Troubleshooting
"No conference bridge profile found" Error
app_confbridge.c: Conference bridge profile 'standard_bridge' not found
Cause: The profile name in the dialplan does not match a profile in confbridge.conf.
Fix: Check for typos. Reload the module:
asterisk -rx "module reload app_confbridge.so"
asterisk -rx "confbridge show profile bridges"
No Audio in Conference
Cause: Usually a timing module issue.
Check:
asterisk -rx "module show like timing"
You need at least one timing module loaded. Preferred order:
res_timing_timerfd(best -- kernel-based, zero overhead)res_timing_dahdi(requires DAHDI kernel module)res_timing_pthread(fallback -- higher CPU, less accurate)
Join Sound Still Playing
Cause: The hear_own_join_sound option is set to yes (default) on the user profile.
Fix: In the customer user profile:
[customer_user]
type=user
hear_own_join_sound=no
quiet=no
The combination of hear_own_join_sound=no on the customer profile and sound_join=sip-silence on the bridge profile ensures complete silence.
ConfBridge Module Not Found
Cause: Asterisk was compiled without ConfBridge support.
Fix: Recompile with app_confbridge enabled in menuselect, or check if the module file exists:
find /usr/lib*/asterisk/modules/ -name "app_confbridge.so"
Conference Monitor Shows "Query Error"
Cause: Database credentials in the monitor script are wrong, or the vicidial_live_agents table does not exist (non-ViciDial system).
Fix: Test the database connection:
mysql -u YOUR_DB_USER -pYOUR_DB_PASS asterisk -e "SELECT 1"
For non-ViciDial systems, the monitor script needs to be adapted to use confbridge list CLI commands instead of database queries.
Multiple Timing Modules Loaded
If you see multiple timing modules loaded, it can cause audio issues. Force a single module:
# Check what is loaded
asterisk -rx "module show like timing"
# In modules.conf, disable unwanted ones:
noload => res_timing_dahdi.so
noload => res_timing_kqueue.so
noload => res_timing_pthread.so
# Keep only res_timing_timerfd.so
High CPU Usage During Large Conferences
Cause: Many participants without silence optimization.
Fix: Enable dsp_drop_silence=yes on all user profiles. This prevents silent audio from being mixed into the conference, dramatically reducing CPU usage for conferences with more than 5-6 participants.
Performance Tuning
Sample Rate
For telephony (G.711, G.729, GSM), use internal_sample_rate=8000. Setting it to auto causes the bridge to negotiate the highest rate, which wastes CPU for voice calls:
[agent_bridge]
type=bridge
internal_sample_rate=8000
Mixing Interval
20(default) -- low latency, slightly higher CPU40-- reduced CPU, small additional delay (acceptable for most deployments)
mixing_interval=20
Silence Detection
Enable on all user profiles for conferences that regularly have more than 3 participants:
dsp_drop_silence=yes
Jitterbuffer
Enable for users on unreliable networks (remote agents, mobile SIP):
jitterbuffer=yes
Disable for local extensions (agents on the same LAN as the server) to minimize latency.
Max Members
Set realistic limits to prevent runaway conferences:
max_members=10 ; Agent conferences (ViciDial)
max_members=50 ; Ad-hoc conference rooms
Security Considerations
PIN protection: Use PINs for external-facing conference rooms to prevent unauthorized access:
[pin_user] type=user pin=YOUR_SECURE_PINContext isolation: Put conference extensions in a dedicated context, not in
defaultorfrom-internal:[conferences] exten => _900X,1,Answer() same => n,ConfBridge(${EXTEN},standard_bridge,standard_user) same => n,Hangup()AMI access control: The
confcronmanager user should have minimal permissions:[confcron] secret = YOUR_STRONG_SECRET read = command,reporting write = command,reporting deny = 0.0.0.0/0.0.0.0 permit = 127.0.0.1/255.255.255.255Monitor script permissions: The PHP monitor script runs as root (via cron) because it needs access to the Asterisk CLI. On a shared system, restrict the file:
chmod 700 /usr/local/bin/conference_monitor.php chown root:root /usr/local/bin/conference_monitor.phpLog file protection: Conference monitor logs may contain phone numbers and agent IDs:
chmod 600 /var/log/conference_monitor.logDatabase credentials: Never put real passwords in scripts distributed to others. Use environment variables or a separate config file with restricted permissions.
What's Next
With ConfBridge configured and monitored, consider these enhancements:
- Web-based conference dashboard: Build a PHP page that shows all active conferences, member counts, and provides kick/mute buttons using the
ConferenceAdminclass from Step 8 - Grafana integration: Export conference metrics (count, duration, member count) to Prometheus and build dashboards
- Call recording integration: Use the
recorded_bridgeprofile for compliance recording of all conference calls - Video conferencing: ConfBridge supports video modes (
follow_talker,sfu) -- enable for WebRTC-connected participants - Whisper mode: Create a user profile that can talk to the agent but not be heard by the customer (coaching mode)
- AMI event streaming: Subscribe to
ConfbridgeJoin,ConfbridgeLeave,ConfbridgeTalkingevents for real-time dashboards
This tutorial is based on production ViciDial deployments running Asterisk 16 and 20 across multiple data centers. The conference monitor script has been running in production since January 2026, processing thousands of agent conferences daily.