Конференц-комнаты на базе Asterisk

в 9:55, , рубрики: asterisk, php, конференции

Предисловие

Добрый день.

На написание данной статьи меня сподвигло 2 вещи: малое количество или вовсе отсутствие современных рабочих примеров по «фишкам» Asterisk, а так же нежелание специалистов делиться этими самыми «фишками» с остальными. Это я сейчас про РУ-комьюнити. Всякие «Деды» на форумах скорее обольют тебя помоями и отправят читать книжки десятилетней давности, чем дадут мало мальски полезную информацию. Сами же темы форумов, созданные в 2005-2010 годах, сильно устарели и иногда что то уже выпилено из текущей версии астериска, а что то надо очень сильно переделать, чтобы заработало.

Так вот.
В следствие отказа от CUCM в пользу Астериска, руководством была поставлена задача сохранить особо популярные у пользователей сервисы в максимально первозданном виде, дабы не третировать людей. Одним из таких было и создание конференций. К тому моменту с Астериском я уже был знаком, но не столь глубоко, по этому на перебор всевозможных вариантов конференции у меня ушло около полутора недель, а на окончательное решение натолкнула вообще другая задача.

Проблема заключается в том, что из похожего на решение есть статья с устаревшей meetme, а так же какой то монстр, которого у меня так и не удалось заставить работать. Я же предлагаю кое что не настолько громоздкое.

Мякотка

Описывать что такое confbridge, за что отвечают секции в том или ином конфиге и что это за опция такая, я не буду, эта информация как раз имеется и актуальна. Сейчас про решение в целом.

Задача: сделать так, чтобы конференцию можно было создать во время разговора, а затем пригласить туда еще абонентов. Основная проблема, что функция channelredirect работает не так как хотелось бы. То есть если выполнять ее из диалплана во время разговора, то один из каналов улетит куда надо, а второй разрушится, а лазать по всему диалплану на 2к строк и прописывать на Dial'ах опцию g было лениво. И мне совершенно не понятно, почему в большинстве мануалов все пытаются решить задачу только через диалплан, игнорируя возможность астериска работать с внешними скриптами и ami.

Итак. Астериск 14.4.0

Скрипт конференции на 2 варианта(c комментариями):

conference.php

<?php

//Готовим коннект к астеру
$host = "192.168.1.1";
$port = "5038";
$timeout = "10";

$user = "conference";
$pass = "1111";



//для того, чтобы не разбивать 1 задачу на 2 скрипта
$kusok = $argv[1];

//Кусок для создания конференции во время разговора
if ($kusok == 1){

//Получаем переменные
$channel = $argv[2];
$bridgepeer = $argv[3];
$confnum = $argv[4];

print_r($bridgepeer);
print_r($confnum);

                //Коннект
                $sconn = fsockopen ($host, $port, $timeout) or die ("Connection to $host:$port failed!");


                                        fputs ($sconn, "Action: Loginrn");
                                        fputs ($sconn, "Username: $userrn");
                                        fputs ($sconn, "Secret: $passrnrn");
                                        //Задаем переменные канала
                                        fputs ($sconn, "Action: Setvarrn");
                                        fputs ($sconn, "Channel: $channelrn");
                                        fputs ($sconn, "Variable: CONFNUMrn");
                                        fputs ($sconn, "Value: $confnumrnrn");


                                        fputs ($sconn, "Action: Setvarrn");
                                        fputs ($sconn, "Channel: $bridgepeerrn");
                                        fputs ($sconn, "Variable: CONFNUMrn");
                                        fputs ($sconn, "Value: $confnumrnrn");
                                        //Редиректим
                                        fputs ($sconn, "Action: Redirectrn");
                                        fputs ($sconn, "Channel: $bridgepeerrn");
                                        fputs ($sconn, "ExtraChannel: $channelrn");
                                        fputs ($sconn, "Context: service_code-aelrn");
                                        fputs ($sconn, "Exten: conferencern");
                                        fputs ($sconn, "Priority: 1rnrn");

                                        fputs($sconn, "Action: Logoffrnrn");
sleep(2);
fclose ($sconn);
}

//Кусок для добавления нового участника
if ($kusok == 2) {

//Получаем переменные
$confnum = $argv[2];
$inviten = $argv[3];

                $sconn = fsockopen ($host, $port, $errno, $errstr, $timeout) or die ("Connection to $host:$port failed!");


                                        //Подключаемся
                                        fputs ($sconn, "Action: Loginrn");
                                        fputs ($sconn, "Username: $userrn");
                                        fputs ($sconn, "Secret: $passrnrn");

                                        //Звоним и закидываем в конфу
                                        fputs ($sconn, "Action: Originatern");
                                        fputs ($sconn, "Channel: Local/".$inviten."@out-aelrn");
                                        fputs ($sconn, "Context: service_code-aelrn");
                                        fputs ($sconn, "Exten: conferencern");
                                        fputs ($sconn, "Priority: 1rn");
                                        fputs ($sconn, "Variable: CONFNUM=".$confnum."rnrn");



                                        fputs($sconn, "Action: Logoffrnrn");
sleep(2);
fclose ($sconn);
}


}

Гуру программирования могут исправить код, сделав из него конфетку, я писал как умел.
Далее начинаем использовать данный скрипт непосредственно в самом Астериске.

Для того, чтобы создать конференцию, я выбрал комбинацию *1. Коротко и не пересекается с основной нумерацией.

Добавляем в features.conf вызов скрипта с передачей в него требуемых переменных

[applicationmap]
conference => *1,self,System(/usr/bin/php /home/script/conference.php 1 ${CHANNEL} ${BRIDGEPEER} ${CALLERID(num)})

Затем, чтобы это срабатывало, создаем в диалплане в секции [globals] переменную и добавляем нашу фичу

DYNAMIC_FEATURES=conference

Для добавления в уже созданную конференцию новых участников, потребуется прописать код в confbridge.conf

[default_menu]
type = menu
*1=dialplan_exec(service_code-ael,conference_add,1)

Ну а теперь самое вкусное, extensions.ael:

Для создание конференции (сюда адресует php скрипт оба разговорных канала):

        conference => {
                ConfBridge(${CONFNUM},,,default_menu);
        }

Для добавления нового пользователя (сюда адресует dialplan_exec):

conference_add => {
                Read(INVITEN,dial,11,i);
                System(/usr/bin/php /home/script/conference.php 2 ${CALLERID(num)} ${INVITEN});
        }

Все. Никаких килотонн кода в диалплане. Все емко. *1 в разговоре и вы в конфе, еще раз *1 гудок и набор номера, кого добавить.

Наросты

Гонимый пожеланиями пользователей, я стал развивать данную фичу.

Следующим стала возможность создавать конференции с нуля (не из разговора), а так же заходить в уже созданные конференции по их номеру, а не ждать приглашающего звонка

Добавляем в диалплан:

_*1XXXX => {
                NoOp(${CONFCHAN});
                Set(__CONFNUM=${EXTEN:2});
                System(/usr/bin/php /home/script/conference.php 3 ${CONFCHAN} ${CONFNUM} );
        }

Добавляем в скрипт:

conference.php

//Для создание конференций с нуля
if ($kusok == 3){

//Получаем переменные
$channel = $argv[2];
$confnum = $argv[3];


                //Коннект
                $sconn = fsockopen ($host, $port, $timeout) or die ("Connection to $host:$port failed!");


                                        fputs ($sconn, "Action: Loginrn");
                                        fputs ($sconn, "Username: $userrn");
                                        fputs ($sconn, "Secret: $passrnrn");
                                        //Задаем переменные канала
                                        fputs ($sconn, "Action: Setvarrn");
                                        fputs ($sconn, "Channel: $channelrn");
                                        fputs ($sconn, "Variable: CONFNUMrn");
                                        fputs ($sconn, "Value: $confnumrnrn");

                                        //Редиректим
                                        fputs ($sconn, "Action: Redirectrn");
                                        fputs ($sconn, "Channel: $channelrn");
                                        fputs ($sconn, "Context: service_code-aelrn");
                                        fputs ($sconn, "Exten: conferencern");
                                        fputs ($sconn, "Priority: 1rnrn");

                                        fputs($sconn, "Action: Logoffrnrn");
sleep(2);
fclose ($sconn);

Так же пришлось доработать строчку _*X.

        _*X. => {
                set(__CONFCHAN=${CHANNEL});
                Dial(Local/${EXTEN}@service_code-ael);

Теперь чтобы войти в конференцию или создать ее с нуля, просто совершается звонок на *1 и номер, например *15234.

Финальной мутацией этого сервиса пока является так называемая «групповая конференция». Это когда большим начальникам лень всех добавлять вручную, а хочется нажать одну кнопку и все в сборе. Для этого я решил сделать отдельные сервис коды (*1XX), чтобы людям и самому не путаться.

Диалплан:

        *100 => {
                Set(CONFNUM=${CALLERID(num)});
                System(/usr/bin/php /home/script/groups.php 0 ${CONFNUM});
                ConfBridge(${CONFNUM},,,default_menu);
        }

Сам скрип сбора участников:

groups.php

//Функция звонка
function call ($group, $confnum) {

        $many = count($group);
                //Цикл разбора массива номеров группы на номера
                for ($i=0; $many>$i; $i++) {

                        //достаем из массива номера
                        $num = trim(array_shift($group[$i]));
                        //Звоним
                        system("asterisk -rx "channel originate Local/$num@out-ael application ConfBridge $confnum"");
                }
}


//функция получения массива группы

function conf_group ($groupid) {

                //Коннектим в базу
                $opt = array(
                    PDO::ATTR_ERRMODE  => PDO::ERRMODE_EXCEPTION,
                    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
                );

                $pdo = new PDO("odbc:mssql_asterisk, "asterisk, "121212", $opt);


                        //Селектим из базы
                          $sql = "SELECT extension FROM [asterisk].[dbo].[conf_groups] where groupid = $groupid";

                          $select = $pdo->query($sql);
                          $result = $select->fetchAll();

                //Дисконенктим
                $pdo = NULL;
                return $result;
}

//Получаем значение группы, которой надо позвонить
$groupid = $argv[1];

if ($groupid == 0){

        //Получаем номер конференции
        $confnum = $argv[2];


        //Получаем массив номеров
        $group=conf_group($groupid);

        //Звоним
        call($group, $confnum);

}

Все группы хранятся в базе данных по структуре «Группы, номер, имя, описание». Если появляется новая группа, просто копипастится кусок с if в коде и exten в диалплане. Циферка меняется на +1 и все готово.

Теперь для сбора, например, всех директоров на совещание, генеральному всего лишь потребуется набрать *100. А как правило у больших боссов большие телефоны. Следовательно биндим *100 на любую клавишу speeddial'а, подписываем как «директора» и пользователь вообще не заморачивается, что ему набрать. Кнопку нажал — совещание собрал.

Все группы хранятся в базе данных по структуре «Группы, номер, имя, описание». Если появляется новая группа, просто копипастится кусок с if в коде и exten в диалплане. Циферка меняется на +1 и все готово.

Теперь предвосхищая ваши вопросы:

Почему скрипты и ami? Потому что средствами диалплана у меня так и не получилось сделать вменяемый редирект обоих каналов, не растеряв их по дороге. В ami же в функции redirect можно прицепить доп канал + задать ему переменную (например номер конференции, чтобы он тоже мог кого то в нее добавить).

Так же вы могли заметить, что я вынес фичи в отдельный контекст service_code-ael. Это удобно, когда всяких фич у вас становится больше пары штук. Я решил делать их через *, следовательно в любом контексте я просто пишу _*X. и адресую в этот контекст. Возможно, кто то найдет решение изящнее, но я за несколько месяцев так и не нашел. А данный функционал пришелся пользователям по душе.

Почему ael, а не conf? Ну потому что он структурированней и читать его легче
и понятнее. Одна функция gotoif чего стоит. До lua я еще не дорос.

Почему в примере массового сбора originate сделаны через bash, а не через AMI? Проблема в том, что выполняя кучу originate подряд через ami, система ждет, пока завершится предыдущий, чтобы дать следующий. А если никто не берет трубку, а там 20 секунд no_ans и таких 5 штук? Можно будет до вечера ждать сбора.

Ну вот, пожалуй, и все. Надеюсь эта статья поможет таким же ищущим, каким был я, когда все это потребовалось сделать быстро, комфортно для пользователей, а главное, чтобы в дальнейшем обслуживать данную систему было удобно самому, так сказать с заделом на будущее.

Автор: MerZoTTa

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js