Взаимодействие с сервером Asterisk из java-приложения через Asterisk Managment Interface (AMI)
Если вы только начинаете исследования в этой области, то взаимодействие с данным сервером может показаться вам несколько запутанным, как когда-то показалось мне.
Чтобы не искать нужные крупицы информации на форумах в стиле ответ-вопрос, прилагаю небольшой туториал о взаимодействии с сервером Asterisk из java.
Важно: я предполагаю, что раз вы дошли до стадии написания кода, то у вас уже есть работающий Asterisk сервер, к которому можно обращаться.
1) Что выбрать, чтобы было удобно работать?
Определенно — Asterisk Managment Interface (AMI): данный интерфейс обладает полным набором функций, позволяющим совершать звонок, слушать события в реальном времени с сервера, получать статус звонка и прерывать его по необходимости.
2) Какую библиотеку подключать?
Вот эту:
<dependency> <groupId>org.asteriskjava</groupId> <artifactId>asterisk-java</artifactId> <version>2.0.4</version> </dependency>
3) Какие конфиги необходимо посмотреть на сервере?
extensions.conf — конфиг, который описывает диаплан. Вы к нему будете постоянно обращаться. Если более понятным языком, то там содержаться сценарии того, что будет делать сервер, при поступлении на него звонка на определенный номер. Сначала в диаплане ищется конкретный контекст — он записывается в квадратных скобочках, после этого под тегом этого контекста ищется номер, по которому вы обращаетесь.
manager.conf — конфиг с юзером и паролем к вашему серверу Asterisk
Содержание данного конфига должно быть примерно следующим:
[user_name] secret = password read = all write = all deny=0.0.0.0/0.0.0.0 permit=0.0.0.0/255.255.255.0
- user_name — имя пользователя
- secret — пароль для него
- deny — ip адреса, доступ которым запрещен под данным пользователем
- permit — доступ которым разрешен. Обязательно указывайте ip, с которого обращаетесь, в permit, так как астер может отбить ваш запрос.
sip.conf — тут прописаны все транки. Транк — это телефон, с которого будем звонить клиенту.
4) С чего начинать писать код?
Тут два варианта: вам либо нужно совершать какие-то действия на сервере Asterisk, либо слушать события на сервере. Наша последовательность включает и то и другое.
Опишем план действий:
- Открываем конекшен к серверу;
- Описываем сценарий работы;
- Слушаем события;
- Закрываем конекшен.
Соответственно, соединение инициализируется во время создания объекта DefaultAsteriskServer:
import org.asteriskjava.live.AsteriskServer; import org.asteriskjava.live.DefaultAsteriskServer;
AsteriskServer asteriskServer = new DefaultAsteriskServer(HOSTNAME, USERNAME, PASSWORD); asteriskServer.initialize();
После того как открыли соединение, нам нужно позвонить пользователю. Назовем это сценарием действий. Описание сценария работы будет в отдельном классе:
/** * Задается сценарий прозвона */ public class ScenarioCall extends OriginateAction { private final Logger log = LoggerFactory.getLogger(ScenarioCall.class); private String TRUNK; private final String PHONE_FOR_RINGING; private final String EXTEN_FOR_APP; private final String CONTEXT_FOR_APP; public ScenarioCall(String trunk, String phoneForRinging, String extension, String context) { this.TRUNK = trunk; this.PHONE_FOR_RINGING = phoneForRinging; this.EXTEN_FOR_APP = extension; this.CONTEXT_FOR_APP = context; this.init(); } /** * инициализируем сценарий и уже в конструкторе получаем готовый OriginateAction */ private void init() { //номер абонента String callId = ValidValues.getValidCallId(this.PHONE_FOR_RINGING); //канал с которого звоним String channelAsterisk = ValidValues.getValidChannel(this.TRUNK, this.PHONE_FOR_RINGING); this.setContext(CONTEXT_FOR_APP); this.setExten(EXTEN_FOR_APP); this.setPriority(1); this.setAsync(true); this.setCallerId(callId); this.setChannel(channelAsterisk); log.info("Create Scenario Call: phone '{}',chanel '{}',context '{}',extension '{}'", callId, channelAsterisk, CONTEXT_FOR_APP, EXTEN_FOR_APP); } }
Что нам нужно понимать в этом сценарии? Сначала создается соединение с транком. Транк — это номер с которого вы будете звонить абоненту. После этого создается соединение между транком и абонентом, уже после этого соединение между абонентом и еще кем там Вам нужно.
Именно в такой последовательности.
Передаваемое значение: контекст, в котором будем искать телефонный номер, с которым вы хотите связать абонента (из extensions.conf).this.setContext(CONTEXT_FOR_APP)
Передаваемое значение: сценарий, который выполнится после того, как вы связались с абонентом (из extensions.conf).this.setExten(EXTEN_FOR_APP)
Передаваемое значение: номер нашего абонентаthis.setCallerId(callId)
Передаваемое значение: устанавливаемый канал связи, обычно выглядит так: trunk_name/phone_user.this.setChannel(channelAsterisk)
Где искать trunk_name? На сервере астериск есть конфиг sip.conf — там прописаны все транки.
Создадим звонок:
if (asteriskServer .getManagerConnection().getState().equals(ManagerConnectionState.CONNECTED) || asteriskServer .getManagerConnection().getState().equals(ManagerConnectionState.CONNECTING) || asteriskServer .getManagerConnection().getState().equals(ManagerConnectionState.INITIAL)) { try { ScenarioCall scenarioCall = new ScenarioCall(trank, phone, extension, context); CallBack callBackForScenarioCall = new CallBack(); asteriskServer.originateAsync(scenarioCall, callBackForScenarioCall); } catch (ManagerCommunicationException e) { //при падении канала связи, StateConnection может быть в RECONNECTING, а может вообще отвалиться } }
Мы создали звонок, но как за ним следить динамически?
Для этого делается две вещи: в методе originateAsync передается экземпляр класса CallBack
и на сервер вешается слушатель, который будет сливать нам все происходящее.
Слушатель нужен, потому что класс CallBack не оповестит вас о окончании звонка, когда пользователь уже поговорил, а так же не оповестит вас о том, что пользователь мог еще куда бы то ни было перевестись.
/** * После того как вы передали в метод asteriskConnection.originateAsync экземпляр * класса CallBack - начнет исполняться сценарий на обзвон, переданный во втором параметре * в originateAsync. CallBack будет служить своеобразным слушателем исполнения звонка, * то если если пользователь не возьмет трубку, будет вызван метод onNoAnswer , если * линия будет занята то onBusy, если канал связи будет недоступен, то onFailure, и тд. * Там вы пишете обработку событий призошедших со звонком. Важно, что данный класс не оповестит * вас об успешном окончании звонка ( то есть когда пользователь взял трубку, поговорил и завершил звонок) */ public class CallBack implements OriginateCallback { /** * Поставим первоначальный статус в PRERING, потом при исполнении переопределенных методов класса * OriginateCallback - можете его менять */ private ChannelState resultCall = ChannelState.PRERING; /** * когда мы звоним абоненту, устанавливается этот статус. Сценарий на обзвон еще не закончил свое выполнение */ @Override public void onDialing(AsteriskChannel asteriskChannel) { // канал связи создан, переустанавливаете resultCall, // важно что asteriskChannel будет скорее всего null, // так что устанавливать resultCall придется хардкодом // обработка события } /** * Абонент взял трубку. Сценарий на обзвон закончил исполнение * устанавливаем статус данного сценария в 6 - setStatus */ @Override public void onSuccess(AsteriskChannel asteriskChannel) { // пользователь поднял трубку, asteriskChannel уже не null, // asteriskChannel.getState() будет скорее всего в значении ChannelState.UP // обработка события } /** * Аббонент не ответил или сбросил звонок, не подняв трубку * устанавливаем статус данного сценария в 7 - setStatus (рекомендуется) */ @Override public void onNoAnswer(AsteriskChannel asteriskChannel) { // пользователь не ответил, // важно что asteriskChannel будет скорее всего null, // так что устанавливать resultCall придется хардкодом // обработка события } /** * Линия занята * устанавливаем статус данного сценария в 7 - setStatus (рекомендуется) */ @Override public void onBusy(AsteriskChannel asteriskChannel) { // телефонная линия занята, // важно что asteriskChannel будет скорее всего null, // так что устанавливать resultCall придется хардкодом // обработка события } /** * Произошла ошибка во время обзвона */ @Override public void onFailure(LiveException e) { // обязательно проводите обработку данного события, // потому что как показала практика, // onFailure будет у вас очень часто } }
Как повесить слушателя на Астериск?
Для этого нужно создать класс имплементирующий AsteriskServerListener, PropertyChangeListener.
Для созданного соединения посредством экземпляра класса AsteriskConnection осуществляем:
this.asteriskConnection.addAsteriskServerListener(this.callBackEventListener);
this.callBackEventListener — экземпляр класса нашего слушателя, рождается из:
** * Слушатель для сервера Asterisk * имплементация PropertyChangeListener нужна для того, чтобы слушать события с сервера. * имплементация AsteriskServerListener нужна для того, чтобы повесить слушателя на AsteriskConnection. */ public class CallBackEventListener implements AsteriskServerListener, PropertyChangeListener { public void onNewAsteriskChannel(AsteriskChannel channel) { channel.addPropertyChangeListener(this); } public void onNewMeetMeUser(MeetMeUser user) { user.addPropertyChangeListener(this); } public void onNewQueueEntry(AsteriskQueueEntry user) { user.addPropertyChangeListener(this); } public void onNewAgent(AsteriskAgent asteriskAgent) { asteriskAgent.addPropertyChangeListener(this); } /** * Ловит событие окончания звонка. С помощью {@link PropertyChangeEvent} * можно отслеживать любые события, * но в данном контексте необходимы только события окончания, * так как события начала звонка слушает класс CallBack * * @param propertyChangeEvent событие происходящее в течении звонка */ public void propertyChange(PropertyChangeEvent propertyChangeEvent) { findEventEndCall(propertyChangeEvent); } private void findEventEndCall(PropertyChangeEvent event) { if (event.getSource() instanceof AsteriskChannel) { AsteriskChannel callBackChannel = (AsteriskChannel) event.getSource(); String callId = getStringWithOnlyDigits(callBackChannel.getCallerId().toString()); callId = ValidValues.getValidCallId(callId); if (callBackChannel.getState().toString().equals("HUNGUP") && event.getOldValue().toString().contains("RINGING")) { //пользователь не поднял трубку или сбросил callBackChannel.removePropertyChangeListener(this); // пишете обработку окончания звонка } else if (callBackChannel.getState().toString().equals("HUNGUP") && event.getOldValue().toString().contains("UP")) { //пользователь поднял трубку и поговорил callBackChannel.removePropertyChangeListener(this); // пишете обработку окончания звонка } else if (callBackChannel.getState().toString().equals("HUNGUP")) { // завершение звонка по другой причине callBackChannel.removePropertyChangeListener(this); // пишете обработку окончания звонка } } } private String getStringWithOnlyDigits(String strForParse) { String result = ""; if (strForParse != null && !strForParse.isEmpty()) { CharMatcher ASCII_DIGITS = CharMatcher.anyOf("<>").precomputed(); result = ASCII_DIGITS.removeFrom(strForParse.replaceAll("[^0-9?!]", "")); } return result; } }
Советую в самом начале просто залогировать, то что приходит в propertyChange и посмотреть на PropertyChangeEvent, это будет адская портянка всего, что происходит на сервере. Он вообще никак не фильтрует информацию. Поэтому вывод: вешать слушателя надо как можно реже. Не на каждый звонок, потому что это можно сделать даже в классе OriginateCallback, насколько я находила. Это ни к чему. Посмотрите, какие вам приходят объекты PropertyChangeEvent, посмотрите какого типа там поля и какие из них вам нужны. Дальше — welcome в мир обработки информации.
Немного о валидации данных.
В OriginateAction.setChannel — передается trunk_name/phone_user
phone_user — если российский, то должен начинаться с восьмерки, если международный номер — с плюса.
В OriginateAction.setCallerId — передается номер телефона клиента,
потом в CallBackEventListener он придет в callBackChannel.getCallerId().
Будет брать его так:
String callId = getStringWithOnlyDigits(callBackChannel.getCallerId().toString());
В итоге не забываем про:
asteriskServer.shutdown();
Если вам нужно прервать какой-либо звонок, то либо в классе CallBackEventListener
на существующий канал связи выполняем:
callBackChannel.hangup();
Такой нехитрый получился туториал. С первого взгляда, конечно, очень просто, но поверьте, нужно много времени и нервов, чтобы найти информацию, отдебажить все методы и оставить работающие.
Успехов вам в работе с серверами Asterisk!
Дополнительная литература:
1) Asterisk-Java tutorial
2) Asterisk Managment Interface (AMI)
