Translate

jueves, 25 de julio de 2013

Sistema de Atención al Cliente con WebRTC y Elastix-CallCenter.

Hola, en este artículo vamos a crear un sistema de atención a cliente usando las herramientas WebRTC-SIPML5 y Elastix junto con su addon de Call Center. La idea general es generar 0 costos entre el usuario y nuestro centro de atención. Es por esto que vamos a usar WebRTC y Elastix dos herramientas open source las cuales van a interactuar usando nuestra conexión de internet.

Paso 1.

Instalar el soporte de WebRTC para el módulo de Call Center de Elastix que publiqué anteriormente.

Paso 2.

Crear una cola de atención:
  • Crear al menos 2 dispositivos SIP. Menú PBX--->Extensions.
  • Crear una cola de atención. Menú PBX--->Queue.
  • Asignar a la nueva cola de atención uno de los dispositivos SIP como miembro dinámico y usando el prefijo S, por ejemplo, para el dispositivo 1500 quedará de la siguiente manera: S1500,0
  • Seleccionar el 'ring strategy'(estrategia de timbrado) con 'fewestcalls'(menos llamadas recibidas).
  • Añadir los Anuncios necesarios.
  • Aplicar los cambios. 

Paso 3.

Configurar el Módulo de Call Center de Elastix, para recibir llamadas:
  • Ir al menú 'Agent Options'(Opciones de agente).
  • Crear al menos una 'callback extension'(Extension callback). Tiene que coincidir con el dispositivo que se añadió como miembro a la cola de atención.
  • Ir a 'Ingoing calls'(llamadas entrantes)---> Queues(Colas).
  • Seleccionar la Cola de atención previamente creada.

Paso 4.

Crear nuestra página web que será desde donde nuestros usuarios van a contactarnos(el diseño de la página web queda al gusto y capacidades de cada quien). 

Para usar la API de SIPML5 pueden checar el ejemplo online con el que ellos cuentan o su documentación. Les dejo el código con el que llevo trabajando en algunos proyectos y el cual estoy usando en este artículo:

<head>
       <title>Navaismo's ElastixCC-WebRTC</title>
    <!-- SIPMl5 API for WEBRTC calls -->
        <script src="js/SIPml-api.js"></script>
        <link href="css/bootstrap.min.css" media="screen" rel="stylesheet"></link>
</head>

<script>
      

 //Variables
 var mySipStack;
        var mycallSession;
 var myregisterSession;

 // readycallback for INIT
 var readyCallback = function(e){
  console.log("engine is ready");
  
  //CHeck if the SIPml start
  if(SIPml.isInitialized() == 1){
   console.log("Done to initialize the engine");
   //If the stack is started, create the sip stack
   startSipStack();
  }else{
   //If not started display console msg
   console.log("Failed to initialize the engine");
  }          
 }

 // error callback for INIT
 var errorCallback = function(e){
  console.error('Failed to initialize the engine: ' + e.message);
 }

 //INIT SIPML5 API
 SIPml.init(readyCallback, errorCallback);

 //Here we listen stack messages
 function listenerFunc(e){
  //Log incoming messages
  tsk_utils_log_info('==stack event = ' + e.type);
  
  switch(e.type){
  
   //If failed msg or error Log in console & Web Page
   case 'failed_to_start': case 'failed_to_stop':  case 'stopping': case 'stopped': {

    console.log("Failed to connect to SIP SERVER")
    mycallSession = null;
    mySipStack = null;
    myregisterSession = null;
 
    $("#mysipstatus").html('');
    $("#mysipstatus").html('<i>Disconnected: </i>'+e.description);
    
    break;
   }

   //If the msg is 'started' now try to Login to Sip server           
          case 'started': {
                  console.log("Trying to Login");
                 
    login();//function to login in sip server
 
    //Display msg in the web page
    $("#mysipstatus").html('');
    $("#mysipstatus").html('<i>Trying to Connect</i>');
   
    break;
   }

   //If the msg 'connected' display the register OK in the web page 
   case 'connected':{
    $("#mysipstatus").html('');
    $("#mysipstatus").html('<i>Registered with Sip Server</i>');

    break;
   }

   //If the msg 'Sent request' display that in the web page---Pattience
   case 'sent_request':{

    $("#mysipstatus").html('');
    $("#mysipstatus").html('<i>'+e.description+'</i>');

    break;
   }

   //If the msg 'terminated' display that on the web---error maybe?
   case 'terminated': {
    $("#mysipstatus").html('');
    $("#mysipstatus").html('<i>'+e.description+'</i>');
    
    break;
   }

   //If the msg 'i_new_call' the browser has an incoming call
   case 'i_new_call': {
     if (mycallSession) {
                          // do not accept the incoming call if we're already 'in call'
                          e.newSession.hangup(); // comment this line for multi-line support
                  }else{
                                
     mycallSession = e.newSession;
    
     //Change buttons values
                   btnCall.value = 'Answer';
                  btnHangUp.value = 'Reject';
                          btnCall.disabled = false;
                          btnHangUp.disabled = false;
 
     //Start ringing in the browser
                          startRingTone();
    
     //Display in the web page who is calling
                   var sRemoteNumber = (mycallSession.getRemoteFriendlyName() || 'unknown');
                   $("#mycallstatus").html("<i>Incoming call from [<b>" + sRemoteNumber + "</b>]</i>");
                   showNotifICall(sRemoteNumber);
    }
    break;
   }

   case 'm_permission_requested':{
                         break;
                 }
              case 'm_permission_accepted':
          case 'm_permission_refused': {
    if(e.type == 'm_permission_refused'){

            btnCall.value = 'Call';
            btnHangUp.value = 'HangUp';
            btnCall.disabled = false;
            btnHangUp.disabled = true;

            mycallSession = null;

            stopRingbackTone();
            stopRingTone();

             $("#mysipstatus").html("<i>" + s_description + "</i>");

                      }
                      break;
                 }
   case 'starting': default: break;
         }            
 }

 //Function to Listen the call session events
 function calllistener(e){
  //Log all events
  tsk_utils_log_info('****call event**** = ' + e.type);

  switch(e.type){

   //Display in the web page that the call is connecting
   case 'connected': case 'connecting': {

        var bConnected = (e.type == 'connected');
                      if (e.session == myregisterSession) {                          
                           $("#mycallstatus").html("<i>" + e.description + "</i>");
    
                      }else if (e.type == 'connecting') {                          
                           $("#mycallstatus").html("<i>" + e.description + "</i>");
                  
    }else if (e.session == mycallSession) {
                          btnHangUp.value = 'HangUp';

                         if (bConnected) {
                           stopRingbackTone();
                           stopRingTone();
     }
                             }
    break;
                        }
  
   //Display in the browser teh call is finished
   case 'terminated': case 'terminating': {
  
    if (e.session == mycallSession) {
                          mycallSession = null;
                         myregisterSession = null;

                          $("#mycallstatus").html("<i>" + e.description + "</i>");
            stopRingbackTone();
            stopRingTone();


                 }else if (e.session == mycallSession) {

            btnCall.value = 'Call';
            btnHangUp.value = 'HangUp';
            btnCall.disabled = false;
            btnHangUp.disabled = true;

            mycallSession = null;

            stopRingbackTone();
            stopRingTone();
                        }
    break;
                
   }  

   // future use with video
           case 'm_stream_video_local_added': {
                      if (e.session == mycallSession) {

                          }
                      break;
                 }
 
   //future use with video
                case 'm_stream_video_local_removed': {
                      if (e.session == mycallSession) {

                          }
                      break;
                 }
   
   //future use with video
         case 'm_stream_video_remote_added':  {
                      if (e.session == mycallSession) {

                          }
                         break;
              }
  
   //future use with video
          case 'm_stream_video_remote_removed': {
                      if (e.session == mycallSession) {

                          }
                      break;
                 }
 
   //added media audio todo messaging
                 case 'm_stream_audio_local_added':
   case 'm_stream_audio_local_removed':
          case 'm_stream_audio_remote_added':
          case 'm_stream_audio_remote_removed': {
         
                  stopRingTone();                   
                         stopRingbackTone();
 
                      break;
                 }

   //If the remote end send us a request with SIPresponse 18X start to ringing
   case 'i_ao_request':{
                         var iSipResponseCode = e.getSipResponseCode();
                         if (iSipResponseCode == 180 || iSipResponseCode == 183) {
                   startRingbackTone(); //function to start the ring tone
     $("#mycallstatus").html('');
                              $("#mycallstatus").html('<i>Remote ringing...</i>');
     $("#btnHangUp").show();
                         }
    break;
   }
           
   // If the remote send early media stop the sounds
   case 'm_early_media': {
                  if (e.session == mycallSession){ 
           stopRingTone();                   
                          stopRingbackTone();
     $("#mycallstatus").html('');
     $("#mycallstatus").html('<i>Call Answered</i>');
    }
    break;
   }
                }

 }

 //function to send the SIP Register
 function login(){
  //Show in the console that the browser is trying to register
  console.log("Registering");
  
  //create the session
         myregisterSession = mySipStack.newSession('register', {
                 events_listener: { events: '*', listener: listenerFunc } // optional: '*' means all events
                });

  //send the register
        myregisterSession.register();
 }

 // function to create the sip stack
 function startSipStack(){
  //show in the console that th browser is trying to create the sip stack
  console.info("attempting to start the SIP STACK");

  //stack options
  mySipStack  = new SIPml.Stack({
          realm: 'asterisk',
          impi: 'usuario',
          impu: 'sip:usuario@myip',
          password: 'mipassword', // optional
          display_name: 'TS', // optional
          websocket_proxy_url: 'ws://miip:10060', // optional
          outbound_proxy_url: 'udp://miip:5060', // optional
          //ice_servers: [{ url: 'stun:stun.l.google.com:19302'}, { url:'turn:user@numb.viagenie.ca', credential:'myPassword'}], // optional
          enable_rtcweb_breaker: true, // optional
          enable_click2call: false, // optional
          events_listener: { events: '*', listener: listenerFunc }, //optional
          sip_headers: [ //optional
              {name: 'User-Agent', value: 'DM_SIPWEB-UA'}, 
              {name: 'Organization', value: 'Digital-Merge'}
          ]
      });
  //If the stack failed show errors in console
  if (mySipStack.start() != 0) {
                 console.info("Failed to start Sip Stack");
             }else{
                 console.info("Started the Sip Stack");
  }
 
 }


 //Fucntion to call/answer
 function call(){
  var calltype;

  if(mySipStack){
   //create the session to call
          mycallSession = mySipStack.newSession('call-audio', {
                  audio_remote: document.getElementById('audio_remote'),
                  audio_local: document.getElementById('audio_local'),
                  video_remote: document.getElementById('video_remote'),
                  video_local: document.getElementById('video_local'),
                      events_listener: { events: '*', listener: calllistener } // optional: '*' means all events
                 });
   $("#mycallstatus").show();
   //call using the number 80000
          mycallSession.call("80000");
  }else if(!mySipStack){
   alert('Stack not ready!');
  
  //If the textbox is empty and the button call is ANSWER, then is a incoming call
  }else if(flag =='Answer' && mySipStack && mycallSession){

                        stopRingbackTone();
                        stopRingTone();                   

   //Accept the session call
   mycallSession.accept({
                                audio_remote: document.getElementById('audio_remote'),
                                audio_local: document.getElementById('audio_local'),
                                events_listener: { events: '*', listener: calllistener } // optional: '*' means all events
                        });
  }
 }


 //function to hangup the call
 function hangup(){
  //If exist a call session, hangup and reset button values
  if(mycallSession){
          mycallSession.hangup({events_listener: { events: '*', listener: calllistener }});
                        stopRingbackTone();
                        stopRingTone();                   
                        btnCall.value = 'Call';
                        btnHangUp.value = 'HangUp';
   $("#callnumber").attr('value','');
   $("#mycallstatus").html("Call Terminated")
   $("#btnHangUp").hide();
   //destroy the call session
   mycallSession = null;

  }else{
   $("#callnumber").attr('value','');
  }   
  
 }

 //Fucntion to send DTMF frames
 function sipSendDTMF(c){
         if(mycallSession && c){
              if(mycallSession.dtmf(c) == 0){
                  try { dtmfTone.play(); } catch(e){ }
              }
         }else{
   var lastn = $("#callnumber").val();

   $("#callnumber").val( lastn + c );
          try { dtmfTone.play(); } catch(e){ }

  }
  
   
     }


/**************** fucntion to play sounds *******************/
    function startRingTone() {
        try { ringtone.play(); }
        catch (e) { }
    }

    function stopRingTone() {
        try { ringtone.pause(); }
        catch (e) { }
    }

    function startRingbackTone() {
        try { ringbacktone.play(); }
        catch (e) { }
    }

    function stopRingbackTone() {
        try { ringbacktone.pause(); }
        catch (e) { }
    }


   function showNotifICall(s_number) {
        // permission already asked when we registered
        if (window.webkitNotifications && window.webkitNotifications.checkPermission() == 0) {
            if (oNotifICall) {
                oNotifICall.cancel();
            }
            oNotifICall = window.webkitNotifications.createNotification('images/sipml-34x39.png', 'Incaming call', 'Incoming call from ' + s_number);
            oNotifICall.onclose = function () { oNotifICall = null; };
            oNotifICall.show();
        }
    }

</script>

<div class="col">
            <h2>Llamenos Usando WebRTC</h2>
ll <audio id='audio_remote'></audio>
                        &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
   <button class="btn btn-primary" id="btnCall" onclick="call()" >Click Aquí Para Llamarnos!</button>
                        <br> &nbsp;&nbsp;&nbsp; &nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
   <button class="btn btn-danger hide" id="btnHangUp" onclick="hangup()" >Colgar Llamada</button><br>
                        &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp; &nbsp; &nbsp;&nbsp; &nbsp; &nbsp; &nbsp;
   <span class="label hide" id="mycallstatus"></span>
                        <br> &nbsp; &nbsp; &nbsp; &nbsp;&nbsp; &nbsp; &nbsp; &nbsp; 
   <span class="label label-inverse" id="mysipstatus">Si ve esta etiquete usted necesita usar Chrome para llamar</span>
   <br><br>
                        <p style="font-size: 10px;">WebRTC es una nueva tecnología que usa el Navegador Web para establecer una llamada, en este caso la llamada es de Audio y Usted necesita contar con una diadema(o bien micrófono y bocinas) para llamarnos sin costo alguno.<br>Esta tecnología por ahora solo es compatible con el navegador Chrome para descargarlo <a href="">de click aqui.</a>
                </div>
                <div class="col"> 
    <!-- Audios -->
    <audio id="audio_remote" autoplay="autoplay" />
    <audio id="ringtone" loop src="sounds/ringtone.wav" />
    <audio id="ringbacktone" loop src="sounds/ringbacktone.wav" />
    <audio id="dtmfTone" src="sounds/dtmf.wav" />




Si van a usar el ejemplo anterior sólo tienen que editar el siguiente código con los datos de su dispositivo SIP.

 //stack options
mySipStack  = new SIPml.Stack({
          realm: 'asterisk',
          impi: 'usuario',
          impu: 'sip:usuario@myip',
          password: 'mipassword', // optional
          display_name: 'TS', // optional
          websocket_proxy_url: 'ws://miip:10060', // optional
                        outbound_proxy_url: 'udp://miip:5060', // optionalk


Y lo más importante es apuntar el número de la llamada hacia nuestra cola de atención previamente creada, eso se logra editando el código(en éste ejemplo la cola creada tiene el número 80000):

//call using the number 80000
  mycallSession.call("80000");

Al finalizar tendrán algo como esto:



Paso 5.

Probar el sistema. Video Demo(sin audio por problemas técnicos):




Notas.

Es claro que este ejemplo sólo usa un peer SIP, sin embargo estos son algunos hints para crear múltiples usuarios.
  • Crear una página web de atención donde primero verifique los peers registrados.
  • Basados en el punto anterior elegir qué peer registrar.
  • Crear un formulario para que el usuario registre algunos datos básicos como su nombre y pasar estos datos con el peer de registro.
  • Adaptar realtime o bien escribir directamente en la base de datos de asterisk para crear peers 'dinámicos'.
  • Crear una página web para clientes/usuarios registrados y crear extensiones/peers basados en estos datos, así mismo se pueden crear campañas entrantes con estos datos. 

El registro no es necesario realmente, pero de esta forma demuestro que es posible interactuar con asterisk y WebRTC.
WebRTC no necesita de Asterisk para lograr esto, de hecho lo puede hacer Peer to Peer(punto a punto) como lo haría la fantástica aplicación llamada Twelephone, sin embargo este artículo esta diseñado para integrar un sistema de atención online con Elastix y su módulo de call center.
Se puede añadir video a las llamadas ;).
Asterisk Plano también es capaz de esto(y más) usando extensiones directamente o bien una cola de atención. Es solo que estoy de buenas con Elastix. :D
Para aquellos que no sólo quieran tener soporte en Chrome existe una extensión que permite a los navegadores como Internet Explorer y Opera funcionar con WebRTC se llama webrtc4all.

Recuerden su feedback es bienvenido.

21 comentarios:

  1. No puedo funcionar correctamente, siempre de la Evaluación "Forbidden".

    ResponderEliminar
    Respuestas
    1. Forbidden, normalmente quiere decir que el peer es incapaz de autenticarse correctamente, revisa los datos de conexión del lado del SIPml5, podría ser que el tu 'realm' no sea asterisk.

      Eliminar
    2. Yo no creo que era sólo por el valor realm "asterisk", estaba usando la ip del servidor, gracias a que está funcionando perfectamente.

      Eliminar
  2. Me pregunto si hay algún secreto para acceder externamente a través ws softphone, entré en mi firewall, solté los puertos necesarios
    5060, 10060, y 50000 a 65000, y no me puedo conectar a mi IP externa en el SIP.

    ResponderEliminar
    Respuestas
    1. No ningún secreto, solo tienes que apuntar a la IP/HOST correcto en el código JS. Abrir los puertos de SIP,WS y RTP que tengas configurados y listo.

      Muchas veces los routers/firewalls tienen opciones de SIP-ALG y SPI activadas y eso afecta la conexión hay que deshabilitarlas y probar.

      Eliminar
    2. siempre me devuelve este error
      __tsip_transport_ws_onerror
      __tsip_transport_ws_onclose

      Eliminar
    3. Estas usando Chrome? Sería bueno que pusieras el log JS completo, normalmente el causante esta líneas arriba de esos mensajes.

      Eliminar
    4. Este comentario ha sido eliminado por el autor.

      Eliminar
    5. Estas recibiendo esto:
      ==stack event = failed_to_start

      Crea un listener para que te diga exactamente cual es el error por el cual no puede iniciar el stack.

      Por lo visto no usas webrtc2sip y te conectas directo a asterisk. Checa que asterisk, la parte ws sea visible también.

      Eliminar
    6. Este comentario ha sido eliminado por el autor.

      Eliminar
    7. Este comentario ha sido eliminado por el autor.

      Eliminar
    8. Este comentario ha sido eliminado por el autor.

      Eliminar
    9. Si estas usando webrtc2sip el websocket debe ser: http://todominio:10060 sin la parte /ws

      Eliminar
    10. También necesitas configurar el NAT de asterisk y de ese peer, la petición se esta enviando pero no regresa nada del lado de asterisk. Entonces configura correctamente tu externhost o externip, localnet y externrefresh además de setear el NAT del peer que estas usando para el api sipml5.

      Eliminar
    11. gracias por la ayuda, me cortaron los comentarios porque de ips ...
      sin duda es un problema de mi servidor de seguridad.

      Eliminar
  3. HOLA, TENGO UNA PREGUNTA, CUANDO INTENTO ENVIAR UN MENSAJE POR CHAT EN WebRTC Agent Console, ME APARECE EL MENSAJE STACK NOT READY, PODRIAS SEÑALARME EL RUMBO CON ESO, TE CUENTO QUE LE SUBI EL ARCHIVO sipml5_elastix_cc-0.1b-DMv1.i386.RPM Y MI ELASTIX ES 2.4.0 CON ASTERISK 1.8.20.0 Y FREEPBX 2.8.1 GRACIAS POR ADELANTADO...

    ResponderEliminar
  4. Esa version no esta soportada usa la version del market place de elastix. Que es la última y baja todas las dependencias.

    El sistema solo funciona con Chrome

    ResponderEliminar
    Respuestas
    1. Hola Max, te cuento que hace unos meses estoy incursionando en Asterisk y Voip, he estado viendo varias de tus publicaciones y me han ayudado mucho, actualmente estoy encarando un proyecto en el que necesito poder establecer llamadas desde una pagina WEB, queria consultarte si me podrias ayudar con este tema, soy de Argentina si tenes una direccion de mail para escribirte y contactarte te lo agradeceria. Saludos.

      Eliminar
  5. Buenas tardes, tengo una duda, aun esta vivo este hilo?

    Tengo muchas dudas acerca del funcionamiento del webrtc2sip, y su configuracion, con que version de Elastix y centos funciona esto ? Gracias

    ResponderEliminar
  6. Hola, alguno tiene el rpm o el zip los enlaces estan rotos, me gustaria instalar API de SIPML5 como lo dice aquí, pero lo instale y algo falta. por eso el favor del enlace. gracias.

    ResponderEliminar
  7. hola, buenos días, estoy usando el código html con asterisk 13 y websocket activo con wss, necesito solamente un click to call en la web y llamar a una extensión virtual en mi asterisk, usando el código html al ejecutar el botón de call visualizo el error stack not ready, por favor podrías ayudarme?, gracias.

    ResponderEliminar