Вступление

Некоторое время назад появилась необходимость интегрировать программный продукт с сервисом voximplant.

Поскольку еще ни разу не приходилось с ним работать, наверное, как и многие, начал искать готовые рабочие примеры интеграции. И каково же было мое удивление, когда оказалось, что информации не так уж и много. А примеров качественного кода и вовсе не было найдено.

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

Способы использования и код

В основном, код заточен под два способа использования:

  1. звонки с сайта на контактный номер;
  2. звонки пользователям(например, с панели администратора).

Конечно же, это совершенно одна и та же задача. Единственное, в первом случае стоит инициализировать клиент для звонков только тогда, когда пользователь непосредственно решил позвонить(поскольку в этот момент появится диалоговое окно браузера о подтверждении доступа к микрофону). Во втором же случае можно клиент инициализировать сразу, поскольку предполагается частое совершение звонков.

js код клиента
/**
 * @constructor
 * @param {String} aLogin_str Логин для приложения voximplant. Обязательный параметр
 * @param {String} aPassword_str Пароль для приложения voximplant. Обязательный параметр
 * @param {Object} aOptOptions_obj Набор необязательных параметров, которые принимаются для метода VoxImplant.getInstance().init(). Список параметров тут: http://voximplant.com/docs/references/websdk/VoxImplant.Config.html
 * 
 * */
function VoximaplantClient(aLogin_str, aPassword_str, aOptOptions_obj) {
    var lLogin_str = aLogin_str;
    var lPassword_str = aPassword_str;
    var lOptions_obj = {
        micRequired: true,
        progressTone: true,
        progressToneCountry: "RU",
        showDebugInfo: false
    };
    if (aOptOptions_obj && aOptOptions_obj instanceof Object) {
        for (var i in aOptOptions_obj) {
            lOptions_obj[i] = aOptOptions_obj[i];
        }
    }

    /**
     * Статус, указывающий, что VoximaplantClient еще не выполнял метод init()
     */
    this.NOT_INTIALIZED = 0;
	
	/**
     * Статус, указывающий, что VoximaplantClient уже выполнил метод init()
     */
    this.INTIALIZED = 1;
	
	/**
     * Статус, указывающий, что у пользователя доступен микрофон. Если микрофон не доступен, статус будет INTIALIZED
     */
    this.MIC_VERIFIED = 2;
	
	/**
     * Статус, указывающий, что клиент подключился к серверу voximplant. Если по каким-то причинам подключиться не удалось, статус будет: MIC_VERIFIED
     */
    this.CONNECTED = 3;
	
	/**
     * Статус, указывающий, что клиент успешно авторизовался на сервере voximplant. 
	 * Если по каким-то причинам авторизоваться не удалось, статус будет: CONNECTED
	 * Единственный статус, при котором возможно совершать звонки
     */
    this.LOGGED_IN = 4;
	
	/**
     * Статус, указывающий, что у клиента подключился к серверу voximplant, но по каким-то причинам подключение разорвалось.
     */
    this.CONNECTION_CLOSED = 5;
    
    /**
     * При начале обработки звонка воксимплантом, обрабатывается эта функция, если ей присвоили значение
     */
    this.onCallingStartedEvent = null;
    
    /**
     * При завершении звонка, обрабатывается эта функция, если ей присвоили значение
     */
    this.onCallingCompletedEvent = null;
    
    /**
     * При окончании работы функции VoximplantClient::init(), обрабатывается эта функция, если ей присвоили значение
     */
    this.onInitializationCompletedEvent = null;
    var self = this;
    var tryToExecuteInitializationCompletedEventHandler = function() {
        if (self.onInitializationCompletedEvent && typeof self.onInitializationCompletedEvent === 'function') {
            self.onInitializationCompletedEvent();
        }
    };

    var lStatus_int = this.NOT_INTIALIZED;
    var lInstance_vx_obj = null;
    var lCall_vx_obj = null;
    
    this.init = function () {
        if (lStatus_int < this.INTIALIZED) {
            lInstance_vx_obj = VoxImplant.getInstance();
            var self = this;
            lInstance_vx_obj.addEventListener(VoxImplant.Events.SDKReady, function() {
                lStatus_int = self.INTIALIZED;
                lInstance_vx_obj.addEventListener(VoxImplant.Events.MicAccessResult, function(e) {
                    if (e.result) {
                        lStatus_int = self.MIC_VERIFIED;
                    } else {
                        tryToExecuteInitializationCompletedEventHandler();
                    }
                });
                if (!lInstance_vx_obj.connected()) {
                    lInstance_vx_obj.addEventListener(VoxImplant.Events.ConnectionEstablished, function() {
                        lStatus_int = self.CONNECTED;
                        lInstance_vx_obj.addEventListener(VoxImplant.Events.AuthResult, function(e) {
                            if (e.result) {
                                lStatus_int = self.LOGGED_IN;
                            }
                            tryToExecuteInitializationCompletedEventHandler();
                        });
                        lInstance_vx_obj.login(lLogin_str, lPassword_str);
                    });
                    lInstance_vx_obj.addEventListener(VoxImplant.Events.ConnectionClosed, function() {
                        lStatus_int = self.CONNECTION_CLOSED;
                    });
                    lInstance_vx_obj.addEventListener(VoxImplant.Events.ConnectionFailed, function() {
                        lStatus_int = self.CONNECTION_CLOSED;
                        tryToExecuteInitializationCompletedEventHandler();
                    });
                    lInstance_vx_obj.connect();
                }
            });
            lInstance_vx_obj.init(lOptions_obj);
        }
    };
    
    /**
     * Получить текущий статус VoximplantClient. 
	 * Доступные статусы: NOT_INTIALIZED, INTIALIZED, MIC_VERIFIED, CONNECTED, LOGGED_IN, CONNECTION_CLOSED
	 * @return {Integer}
     */
    this.getStatus = function() {
        return lStatus_int;
    };
    
    /**
     * Проверяем, разрешен ли звонок
	 * @return {Boolean}
     */
    this.isCallingAllowed = function() {
        return lStatus_int === self.LOGGED_IN;
    };
    
    /**
     * Звоним на номер
	 * 
	 * @param{String} aNumber телефонный номер на который звоним
     */
    this.callToNumber = function(aNumber) {
        if (!this.isCallingAllowed() || lCall_vx_obj !== null) {
            return;
        }
        aNumber = aNumber.replace(/[^0-9]/g, '');
        lCall_vx_obj = lInstance_vx_obj.call(aNumber);
        var self = this;
        var closeCallFunc = function() {
            lCall_vx_obj.removeEventListener(VoxImplant.CallEvents.Failed, closeCallFunc);
            lCall_vx_obj.removeEventListener(VoxImplant.CallEvents.Disconnected, closeCallFunc);
            if (lCall_vx_obj.state() !== 'ENDED') {
                lCall_vx_obj.hangup();
            }
            lCall_vx_obj = null;

            if (self.onCallingCompletedEvent && typeof self.onCallingCompletedEvent === 'function') {
                self.onCallingCompletedEvent();
            }
        };
        lCall_vx_obj.addEventListener(VoxImplant.CallEvents.Failed, closeCallFunc);
        lCall_vx_obj.addEventListener(VoxImplant.CallEvents.Disconnected, closeCallFunc);

        if (this.onCallingStartedEvent && typeof this.onCallingStartedEvent === 'function') {
            this.onCallingStartedEvent();
        }
    };
    
    /**
     * Завершить звонок
     */
    this.hangUp = function() {
        if (!this.isCallingAllowed() || lCall_vx_obj === null) {
            return;
        }
        lCall_vx_obj.hangup();
    };
}


Так как первый вариант использования более комплексный, реализуем его. Для начала создадим такую html-форму для звонков:



код html-формы
<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="utf-8">
		<meta http-equiv="X-UA-Compatible" content="IE=edge">
		<meta name="viewport" content="width=device-width, initial-scale=1">
		<title>Voximplant test page</title>
		<link href="css/bootstrap.min.css" rel="stylesheet">
	</head>
	<body>
		<div class="container">
			<div class="row" style="margin-top:20px;">
				<div class="col-xs-4 col-sm-4 col-md-3 text-right">
					Позвонить на номер:
				</div>
				<div class="col-xs-4 col-sm-4 col-md-3">
					<input type="password" class="form-control" id="phone_num"/>
				</div>
				<div class="col-xs-4 col-sm-4 col-md-3">
					<button type="button" class="btn btn-success" id="call_to_num"><span class="glyphicon glyphicon-earphone"></span></button>
				</div>
			</div>
			<div class="row" style="margin-top:20px;">
				<div class="col-md-12" id="log">
				</div>
			</div>
		</div>
	</body>
</html>


Дальше подключим несколько скриптов: скрипт WebSDK voximplant'а, библиотека jquery, наш voximplantclient, а также скрипт, демонстрирующий работу с voximplantclient.

<script src="//cdn.voximplant.com/voximplant.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script src="js/voximplantclient.js"></script>
<script>
	var voximplantClient = new VoximaplantClient('xxxxx', 'xxxxx');
	voximplantClient.onInitializationCompletedEvent = function () {
		$('#log').append('<p class="bg-warning">Инициализиация завершена</p>');
		callToNumber();
	};
	voximplantClient.onCallingStartedEvent = function () {
		$('#call_to_num').removeClass('btn-success btn-warning').addClass('btn-danger');
		$('#log').append('<p class="bg-danger">Звонок начался</p>');
	};
	voximplantClient.onCallingCompletedEvent = function () {
		$('#call_to_num').removeClass('btn-danger btn-warning').addClass('btn-success');
		$('#log').append('<p class="bg-success">Звонок завершен</p>');
	};
	
	function callToNumber() {
		if (voximplantClient.isCallingAllowed()) {
			voximplantClient.callToNumber($('#phone_num').val());
		} else {
			var voximplantStatus = voximplantClient.getStatus();
			if (voximplantStatus === voximplantClient.NOT_INTIALIZED) {
				voximplantClient.init();
				$('#call_to_num').removeClass('btn-success btn-danger').addClass('btn-warning');
				$('#log').append('<p class="bg-warning">Начата инициализация</p>');
			} else {
				switch (voximplantStatus) {
					case voximplantClient.INTIALIZED:
						msg_text = 'Не удается сделать звонок. Нет доступа к микрофону';
						break;
					case voximplantClient.MIC_VERIFIED:
						msg_text = 'Не удается сделать звонок. Нет соединения с сервером voximplant';
						break;
					case voximplantClient.CONNECTED:
						msg_text = 'Не удается сделать звонок. Не удалось авторизоваться на сервере voximplant';
						break;
					case voximplantClient.CONNECTION_CLOSED:
						msg_text = 'Не удается сделать звонок. Соединение с сервером voximplant закрыто. Пожалуйста, перезайдите в панель администратора и попробуйте снова.';
						break;
				}
				alert(msg_text);
			}
		}
	}
	
	$(document).ready(function () {
		$('#call_to_num').click(function () {
			if ($(this).hasClass('btn-success')) {
				callToNumber();
			} else if ($(this).hasClass('btn-danger')) {
				voximplantClient.hangUp();
			}
		});
	});
</script>

По-моему код получился настолько самодокументируемым и понятным, что комментировать здесь нечего. В итоге получаем:



Достоинства и недостатки

К достоинствам следует отнести:

  • Одновременно можно совершать лишь один звонок;
  • Номер для дозвона можно менять во время работы на сайте;
  • Присутствуют обработчики событий на начало звонка и на окончание звонка;
  • Простой набор интерфейсов;
  • Код написан в ООП-стиле, что сводит к минимуму количество переменных в глобальном пространстве видимости.

Также и имеются недостатки:

  • Неточное описание состояний. Например, когда после инициализации объект находится в состоянии MIC_VERIFIED, на самом деле это означает, что была произведена попытка соединения с сервером voximplant, но соединение не удалось. Да, пришлось пожертвовать неточностью в замен простоты и времени разработки;
  • Клиент имеет функционал только для звонков;
  • Неточность вызова обработчика onCallingStartedEvent. Когда срабатывает данный обработчик, означает, что начался звонок написанным классом, а не начался звонок непосредственно на телефон дозвона;

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

Думаю, кому-нибудь да будет полезно.

Комментарии (0)