← All Tutorials

ConfBridge Conference Rooms + Monitoring for ViciDial/Asterisk

ViciDial Administration Intermediate 36 min read #26

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

  1. Introduction
  2. MeetMe vs ConfBridge
  3. Architecture Overview
  4. Prerequisites
  5. Step 1: Verify ConfBridge Module
  6. Step 2: Configure Bridge Profiles
  7. Step 3: Configure User Profiles
  8. Step 4: Configure Admin Profiles and Menus
  9. Step 5: Dialplan — User and Admin Entry
  10. Step 6: ViciDial ConfBridge Integration
  11. Step 7: Conference Monitor Script
  12. Step 8: Admin Controls — Mute, Kick, Lock
  13. Step 9: Database Logging Table
  14. Step 10: Cron and Automation
  15. Step 11: Testing and Verification
  16. Step 12: Full Migration Script
  17. Troubleshooting
  18. Performance Tuning
  19. Security Considerations
  20. 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:

  1. Setting up ConfBridge with proper profiles for agents, customers, admins, and monitors
  2. Creating dialplan extensions for numbered conference rooms with admin access
  3. 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:

  1. ConfBridge Configuration (confbridge.conf) -- bridge profiles, user profiles, and DTMF menus that define how conferences behave
  2. Dialplan Extensions (extensions.conf or customexte.conf) -- numbered extensions that map dial codes to conference rooms with specific profiles
  3. 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

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-silence sound 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=no on the customer user profile, combined with quiet=no on 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:

  1. Navigate to Admin > Servers
  2. Click on your server entry
  3. Change Conferencing Engine from MEETME to CONFBRIDGE
  4. Set Rebuild conf files to Y
  5. 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

  1. Register two SIP phones (softphones like Zoiper or Linphone work fine)
  2. From Phone A, dial 9000 -- you should enter conference room 9000
  3. From Phone B, dial 9000 -- you should join the same room
  4. Verify both parties can hear each other
  5. Test DTMF: Press 1 to toggle mute, press 0 to leave

Test 3: Admin Controls

  1. From Phone A, dial 9000 (join as regular user)
  2. From Phone B, dial 9100 (join as admin to room 9000)
  3. As admin (Phone B), press 2 to lock the conference
  4. Try joining from a third phone -- it should be rejected
  5. As admin, press 2 again to unlock
  6. As admin, press 3 to kick the last person who joined
  7. As admin, press 8 to mute all non-admin participants

Test 4: Monitor/Listen-Only

  1. From Phone A and Phone B, dial 9000
  2. From Phone C, dial 9400 (monitor room 9000)
  3. Verify Phone C can hear A and B but A and B cannot hear C
  4. 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:

  1. res_timing_timerfd (best -- kernel-based, zero overhead)
  2. res_timing_dahdi (requires DAHDI kernel module)
  3. 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

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

  1. PIN protection: Use PINs for external-facing conference rooms to prevent unauthorized access:

    [pin_user]
    type=user
    pin=YOUR_SECURE_PIN
    
  2. Context isolation: Put conference extensions in a dedicated context, not in default or from-internal:

    [conferences]
    exten => _900X,1,Answer()
    same => n,ConfBridge(${EXTEN},standard_bridge,standard_user)
    same => n,Hangup()
    
  3. AMI access control: The confcron manager 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.255
    
  4. Monitor 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.php
    
  5. Log file protection: Conference monitor logs may contain phone numbers and agent IDs:

    chmod 600 /var/log/conference_monitor.log
    
  6. Database 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:


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.

Need expert help with your setup?

VoIP infrastructure consulting, AI voice agent integration, monitoring stacks, scaling — I've done it all in production.

Get a Free Consultation