Webphone

Xây dựng web SIP Phone dựa trên webrtc (jssip)

Khi bạn có một ứng dụng Web và cần kết thực hiện cuộc gọi với tổng đài thì WebRTC là giao thức để bạn đạt được điều này

Jssip có một bản demo trực tuyến rất tốt. Bạn dùng tài khoản máy nhánh được cấp và kiểm thử tại: https://sbcwrtchcm.ccall.vn/ hoặc https://tryit.jssip.net/

  1. Giới thiệu về kỹ thuật

WebRTC (giao tiếp thời gian thực trên Web) là một API hỗ trợ trình duyệt web thực hiện các cuộc thoại hoặc video trong thời gian thực. Nó là mã nguồn mở vào ngày 1 tháng 6 năm 2011 và được đưa vào tiêu chuẩn khuyến nghị W3C của World Wide Web Alliance với sự hỗ trợ của Google, Mozilla và opera SIP (giao thức khởi tạo phiên) là một giao thức được phát triển bởi nhóm làm việc mmusic của IETF. Theo tiêu chuẩn, SIP được đề xuất để tạo, sửa đổi và chấm dứt các phiên người dùng tương tác bao gồm video, thoại, nhắn tin tức thời, trò chơi trực tuyến và thực tế ảo. Vào tháng 11 năm 2000, SIP chính thức được chấp thuận là một trong những giao thức báo hiệu 3GPP, và trở thành một đơn vị thường trực của kiến trúc IMS. SIP, giống như H.323, là một trong những giao thức báo hiệu quan trọng nhất cho VoIP.

Nói chung, bạn có thể sử dụng điện thoại(thiết bị) hoặc cài đặt phần mềm SIP client vào máy tính để thực hiện cuộc gọi. Giá thành phần cứng của điện thoại cao, trong khi đó phần mềm SIP client thường tương thích kém, không thể sử dụng đa nền tảng, dễ bị phần mềm diệt virus giết chết.

WebRTC có thể là một giải pháp tốt hơn, miễn là trình duyệt có thể thực hiện các cuộc gọi thoại và video trong thời gian thực. Tận dụng được Webbrowser để thực hiện cuộc gọi thì đây là một giải pháp rất tốt. Websocket được sử dụng để truyền tín hiệu SIP, trong khi webrtc được sử dụng để truyền các luồng thoại và video trong thời gian thực.

  1. Lược đồ triển khai webrtc giao diện người dùng

Trên thực tế, chúng ta không cần phải xử lý các phương pháp liên quan đến webrtc, xử lý thoại, video hoặc truyền phát phương tiện. Đã có sẵn các mô-đun (thư viện) miễn phí, nguồn mở tốt trên thị trường rồi.

Đó là JSSIP (jssip.net)

JSSIP

Jssip có các tính năng như sau:

  • Nó có thể được chạy trong trình duyệt hoặc nodejs

  • Sử dụng websocket để tải giao thức SIP

  • Sử dụng webrtc truyền thoại, hình ảnh thời gian thực

  • Tải rất nhẹ

  • 100% JavaScript thuần túy

  • API đơn giản và mạnh mẽ để sử dụng

  • Nhiều máy chủ hỗ trợ Oversip, Kamailio, Asterisk, Officeip, rsiprocate, frafos ABC SBC, teksip

  • Phát triển bởi tác giả của RFC 7118 và oversip

Đây là một ví dụ về việc sử dụng Jssip để gọi điện thoại. Nó rất đơn giản

// Create our JsSIP instance and run it:

var socket = new JsSIP.WebSocketInterface('wss://sip.myhost.com');
var configuration = {
 sockets  : [ socket ],
 uri      : 'sip:alice@example.com',
 password : 'superpassword'
};

var ua = new JsSIP.UA(configuration);

ua.start();

// Register callbacks to desired call events
var eventHandlers = {
 'progress': function(e) {
   console.log('call is in progress');
 },
 'failed': function(e) {
   console.log('call failed with cause: '+ e.data.cause);
 },
 'ended': function(e) {
   console.log('call ended with cause: '+ e.data.cause);
 },
 'confirmed': function(e) {
   console.log('call confirmed');
 }
};

var options = {
 'eventHandlers'    : eventHandlers,
 'mediaConstraints' : { 'audio': true, 'video': true }
};

var session = ua.call('sip:bob@example.com', options);
  1. Các browser hỗ trợ WebRTC

Vì webrtc có yêu cầu cao đối với các trình duyệt nên bạn có thể xem hình bên dưới. Trình duyệt nào hỗ trợ webrtc: Chrome, Firefox, Microsoft Edge

  1. Tài liệu tham khảo và thực hành

  • Js SIP Getting Started, https://jssip.net/documentation/3.8.x/getting_started/

  • SIP protocol status code (lỗi): 422: “Session Interval Too Smal”. Jssip mặc định đặt Session-Expires: 90. Nếu trường thời gian chờ nhỏ hơn giá trị đặt của máy chủ, sẽ có phản hồi 422. Xem mã trạng thái giao thức SIP và bạn có thể đặt sesssionTimersExpires cao hơn giá trị đã đặt của máy chủ

Các code mẫu

Code mẫu JSSIP

 var infocall = {
    wsuri: 'sbcwrtchcm.ccall.vn',
    port: '8080',
    user_extend: '100',
    server: 'epacific.com.vn',
    password: '123456'
};
//login extend in server
var loginExtend = function(){
    var configuration = {
        'ws_servers': 'wss://' + infocall['wsuri'] + ':' + infocall['port']+'/ws',
        'uri': infocall['user_extend'] + '@' + infocall['server'],
        'password': infocall['pass_extend']
    };
    ua = new JsSIP.UA(configuration);
    ua.start();
}
//call out
var outGoing = function(number){
    if(!ua) {
        console.log('Cannot login ua');
        return false;
    }
    else{
        var options = {
            'mediaConstraints': {'audio': true, 'video': false},
            'sessionTimersExpires': 120
        };
        ua.call(number, options);
    }
}
//get info call
var getInfoCall = function(){
    if(!ua) {
        console.log('Cannot login ua');
        return false;
    }
    else {
        ua.on('newRTCSession', function(e){
            console.log(e);
            eventCall(e);
        })
    }
}
//get event call
var eventCall = function(session){
    session['session'].on('progress', function(e){
        console.log('call is in progress');
        console.log(e)
    })
    session['session'].on('failed', function(e){
        console.log('call failed');
        console.log(e)
    })
    session['session'].on('ended', function(e){
        console.log('call end');
        console.log(e);
    })
    session['session'].on('confirmed', function(e){
        console.log('call accept');
        console.log(e)
    })
    session['session'].on('addstream', function(e){
        audio_callcenter.src = window.URL.createObjectURL(e.stream);
    })
}

Code mẫu JSSIP embed gửi callout

var ua;
var ws_was_connected = false;
var ws_registered = false;

jQuery(document).ready(function() {
   var BrowserSignature = webrtcDetectedBrowser +' ('+ webrtcDetectedVersion +')';
   var soundPlayer = document.createElement("audio");
   soundPlayer.volume = 1;

   //var selfView = document.getElementById('selfView');
   //var remoteView = document.getElementById('remoteView');
   var localStream, remoteStream;

   // Flags indicating whether local peer can renegotiate RTC (or PC reset is required).
   var localCanRenegotiateRTC = function () {
       return JsSIP.rtcninja.canRenegotiate;
   };

   window.GUI = {
       jssipCall : function(target) {
           ua.call(target, {
               //pcConfig: peerconnection_config,
               mediaConstraints: { audio: true, video:false },
               extraHeaders: [
                   //'X-Can-Renegotiate: ' + String(localCanRenegotiateRTC())
                   'X-Browser: '+ BrowserSignature
               ],
               rtcOfferConstraints: {
                   offerToReceiveAudio: 1,
                   offerToReceiveVideo: 1
               },
               rtcConstraints: { mandatory: { googIPv6:false } }
           });
       }
       ,
       jssipMessage : function(uri, text) {
           ua.sendMessage(uri,text);
       },


       jssipIsComposing : function(uri, active) {
           //JsSIP.API.is_composing(uri, active);
           //console.info('is compossing..')
       }
       ,
       getSession : function(uri) {
           var session_found = null;

           jQuery("#sessions > .session").each(function(i, session) {
               if (uri === jQuery(this).find(".peer > .uri").text()) {
                   session_found = session;
                   return false;
               }
           });

           if (session_found)
               return session_found;
           else
               return false;
       }
       ,
       createSession : function(display_name, uri) {
           var session_div = jQuery('<div class="session"></div>');

           var session_inner_div = jQuery('\
                   <div class="close"></div> \
                   <div class="container"> \
                     <div class="peer"> \
                       <span class="display-name">' + display_name + '</span> \
                       <span>&lt;</span><span class="uri">' + uri + '</span><span>&gt;</span> \
                     </div> \
                     <div class="call inactive"> \
                       <div class="button dial"></div> \
                       <div class="button hangup"></div> \
                       <div class="button dtmf"></div> \
                       <!--<div class="button hold"></div> \
                       <div class="button resume"></div>--> \
                       <div class="dtmf-box"> \
                         <div class="dtmf-row"> \
                           <div class="dtmf-button digit-1">1</div> \
                           <div class="dtmf-button digit-2">2</div> \
                           <div class="dtmf-button digit-3">3</div> \
                         </div> \
                         <div class="dtmf-row"> \
                           <div class="dtmf-button digit-4">4</div> \
                           <div class="dtmf-button digit-5">5</div> \
                           <div class="dtmf-button digit-6">6</div> \
                         </div> \
                         <div class="dtmf-row"> \
                           <div class="dtmf-button digit-7">7</div> \
                           <div class="dtmf-button digit-8">8</div> \
                           <div class="dtmf-button digit-9">9</div> \
                         </div> \
                         <div class="dtmf-row"> \
                           <div class="dtmf-button digit-asterisk">*</div> \
                           <div class="dtmf-button digit-0">0</div> \
                           <div class="dtmf-button digit-pound">#</div> \
                         </div> \
                       </div> \
                       <div class="call-status"></div> \
                     </div> \
                     <div id="webcam_wrapper"> \
                     <div id="webcam"> \
                     <video id="remoteView" autoplay _hidden=true poster="'+ images[1]+'"></video>\
                     <video id="selfView" autoplay muted="true" _hidden=true poster="'+ images[0]+'"></video>\
                     </div>\
                     </div>\
                     <div class="chat"> \
                       <!--<div class="chatting inactive"></div> \
                       <input class="inactive" type="text" name="chat-input" value="type to chat..."/>--> \
                       <div class="iscomposing"></div> \
                     </div> \
                   </div> \
                  \
             ');

           session_div.css('cursor', 'move').draggable(); //.draggable();
           session_div.append(session_inner_div);
           jQuery("#sessions").append(session_div);
           //jQuery(document).append(session_div);
           var session = jQuery("#sessions .session").filter(":last");
           var call_status = jQuery(session).find(".call");
           var close = jQuery(session).find("> .close");
           //var chat_input = jQuery(session).find(".chat > input[type='text']");
           selfView =   document.getElementById('selfView');
           remoteView =  document.getElementById('remoteView');

           //alert("Self: "+selfView.innerHTML);
           //alert("Remote: "+remoteView.innerHTML);

           options.eventHandlers.confirmed = function(e){
               selfView.src = window.URL.createObjectURL(session.connection.getLocalStreams()[0]);
           }
           options.eventHandlers.addstream  = function(e){
               var stream = e.stream;
           //    alert(stream);
               remoteView.src = window.URL.createObjectURL(stream);
           }
           var div_webcam = jQuery("#sessions").find("#webcam");// jQuery("div#webcam");
           //alert(div_webcam.html());
           div_webcam.show();
           jQuery(session).hover(function() {
                   if (jQuery(call_status).hasClass("inactive"))
                       jQuery(close).show();
               },
               function() {
                   jQuery(close).hide();
               });

           close.click(function() {
               GUI.removeSession(session, null, true);
           });
           /*
           chat_input.focus(function(e) {
               if (jQuery(this).hasClass("inactive")) {
                   jQuery(this).val("");
                   jQuery(this).removeClass("inactive");
               }
           });

           chat_input.blur(function(e) {
               if (jQuery(this).val() == "") {
                   jQuery(this).addClass("inactive");
                   jQuery(this).val("type to chat...");
               }
           });

           chat_input.keydown(function(e) {
               // Ignore TAB and ESC.
               if (e.which == 9 || e.which == 27) {
                   return false;
               }
               // Enter pressed? so send chat.
               else if (e.which == 13 && jQuery(this).val() != "") {
                   var text = chat_input.val();
                   GUI.addChatMessage(session, "me", text);
                   chat_input.val("");
                   GUI.jssipMessage(uri, text);
               }
               // Ignore Enter when empty input.
               else if (e.which == 13 && jQuery(this).val() == "") {
                   return false;
               }
               // NOTE is-composing stuff.
               // Ignore "windows" and ALT keys, DEL, mayusculas and 0 (que no sé qué es).
               else if (e.which == 18 || e.which == 91 || e.which == 46 || e.which == 16 || e.which == 0)
                   return false;
               // If this is the first char in the input and the chatting session
               // is active, then send a iscomposing notification.
               else if (e.which != 8 && jQuery(this).val() == "") {
                   GUI.jssipIsComposing(uri, true);
               }
               // If this is a DELETE key and the input has been totally clean, then send "idle" isomposing.
               else if (e.which == 8 && jQuery(this).val().match("^.$"))
                   GUI.jssipIsComposing(uri, false);
           });
           */
           jQuery(session).fadeIn(100);

           // Return the jQuery object for the created session div.
           return session;

       }
       ,
       setCallSessionStatus : function(cur_session, status, description, realHack) {
           var session = cur_session;
           var uri = jQuery(session).find(".peer > .uri").text();
           var call = jQuery(session).find(".call");
           var status_text = jQuery(session).find(".call-status");
           var button_dial = jQuery(session).find(".button.dial");
           var button_hangup = jQuery(session).find(".button.hangup");
           var button_hold = jQuery(session).find(".button.hold");
           var button_resume = jQuery(session).find(".button.resume");
           var button_dtmf = jQuery(session).find(".button.dtmf");
           var dtmf_box = jQuery(session).find(".dtmf-box");

           // If the call is not inactive or terminated, then hide the
           // close button (without waiting for blur() in the session div).
           if (status != "inactive" && status != "terminated") {
               jQuery(session).unbind("hover");
               jQuery(session).find("> .close").hide();
           }

           // Unset all the functions assigned to buttons.
           button_dial.unbind("click");
           button_hangup.unbind("click");
           button_hold.unbind("click");
           button_resume.unbind("click");
           button_dtmf.unbind("click");

           if (session.call && session.call.status !== JsSIP.C.SESSION_TERMINATED) {
               button_hangup.click(function() {
                   GUI.setCallSessionStatus(session, "terminated", "terminated");
                   session.call.terminate();
                   GUI.removeSession(session, 500);
               });
           }
           else {
               button_dtmf.unbind("click");
           }

           switch(status) {
               case "inactive":
                   call.removeClass();
                   call.addClass("call inactive");
                   status_text.text("");

                   button_dial.click(function() {
                       GUI.jssipCall(uri);
                   });

                   // Hide DTMF box.
                   dtmf_box.hide();
                   break;

               case "trying":
                   call.removeClass();
                   call.addClass("call trying");
                   status_text.text(description || "trying...");

                   // unhide HTML Video Elements
                   //jQuery('#remoteView').attr('hidden', false);
                   //jQuery('#selfView').attr('hidden', false);

                   // Set background image
                   //jQuery('#remoteView').attr('poster', "images/logo.png");

                   // Hide DTMF box.
                   dtmf_box.hide();
                   break;

               case "in-progress":
                   call.removeClass();
                   call.addClass("call in-progress");
                   status_text.text(description || "in progress...");

                   // ring-back.
                   soundPlayer.setAttribute("src", base_url + "embed/sounds/outgoing-call2.ogg");
                   soundPlayer.play();

                   // Hide DTMF box.
                   dtmf_box.hide();
                   break;

               case "answered":
                   call.removeClass();
                   call.addClass("call answered");
                   status_text.text(description || "answered");

                   button_hold.click(function(){
                       if (! session.call.isReadyToReOffer()) {
                           console.warn('Tryit: not ready to reoffer');
                           return;
                       }
                       if (! localCanRenegotiateRTC() || ! session.call.data.remoteCanRenegotiateRTC) {
                           console.warn('Tryit: resetting PeerConnection before hold');
                           session.call.connection.reset();
                           session.call.connection.addStream(localStream);
                       }
                       session.call.hold({useUpdate: false});
                   });

                   button_dtmf.click(function() {
                       dtmf_box.toggle();
                   });

                   if (realHack) { return; }

                   var dtmf_button = jQuery(dtmf_box).find(".dtmf-button");
                   window.dtmf_button = dtmf_button;
                   var sound_file;
                   dtmf_button.click(function() {
                       if (jQuery(this).hasClass("digit-asterisk"))
                           sound_file = "asterisk";
                       else if (jQuery(this).hasClass("digit-pound"))
                           sound_file = "pound";
                       else
                           sound_file = jQuery(this).text();
                       soundPlayer.setAttribute("src", base_url + "embed/sounds/dialpad/" + sound_file + ".ogg");
                       soundPlayer.play();

                       session.call.sendDTMF(jQuery(this).text());
                   });

                   break;

               case "hold":
               case "unhold":
                   if (session.call.isOnHold().local) {
                       call.removeClass();
                       call.addClass("call on-hold");
                       button_resume.click(function(){
                           if (! session.call.isReadyToReOffer()) {
                               console.warn('Tryit: not ready to reoffer');
                               return;
                           }
                           if (! localCanRenegotiateRTC() || ! session.call.data.remoteCanRenegotiateRTC) {
                               console.warn('Tryit: resetting PeerConnection before unhold');
                               session.call.connection.reset();
                               session.call.connection.addStream(localStream);
                           }
                           session.call.unhold();
                       });
                   } else {
                       GUI.setCallSessionStatus(session, 'answered', null, true);
                   }

                   var local_hold = session.call.isOnHold().local;
                   var remote_hold = session.call.isOnHold().remote;

                   var status = "hold by";
                   status += local_hold?" local ":"";

                   if (remote_hold) {
                       if (local_hold)
                           status += "/";

                       status += " remote";
                   }

                   if (local_hold||remote_hold) {
                       status_text.text(status);
                   }

                   break;

               case "terminated":
                   call.removeClass();
                   call.addClass("call terminated");
                   status_text.text(description || "terminated");
                   button_hangup.unbind("click");

                   // Hide DTMF box.
                   dtmf_box.hide();
                   break;

               case "incoming":
                   call.removeClass();
                   call.addClass("call incoming");
                   status_text.text("incoming call...");
                   soundPlayer.setAttribute("src", base_url + "embed/sounds/incoming-call2.ogg");
                   soundPlayer.play();

                   button_dial.click(function() {
                       session.call.answer({
                           //pcConfig: peerconnection_config,
                           // TMP:
                           mediaConstraints: {audio: true, video: false},
                           extraHeaders: [
                               'X-Can-Renegotiate: ' + String(localCanRenegotiateRTC())
                           ],
                           rtcOfferConstraints: {
                               offerToReceiveAudio: 1,
                               offerToReceiveVideo: 1
                           }//,
                       });
                   });

                   // unhide HTML Video Elements
                   //jQuery('#remoteView').attr('hidden', false);
                   //jQuery('#selfView').attr('hidden', false);

                   // Set background image
                   //jQuery('#remoteView').attr('poster', "images/logo.png");

                   // Hide DTMF box.
                   dtmf_box.hide();
                   break;

               default:
                   alert("ERROR: setCallSessionStatus() called with unknown status '" + status + "'");
                   break;
           }
       }
       ,
       /*
        * JsSIP.UA new_session event listener
        */
       newSession : function(e) {

           var display_name, status,
               request = e.request,
               call = e.session,
               uri = call.remote_identity.uri.toString(),
               session = GUI.getSession(uri);

           display_name = call.remote_identity.display_name || call.remote_identity.uri.user;

           if (call.direction === 'incoming') {
               status = "incoming";
               if (request.getHeader('X-Can-Renegotiate') === 'false') {
                   call.data.remoteCanRenegotiateRTC = false;
               }
               else {
                   call.data.remoteCanRenegotiateRTC = true;
               }
           } else {
               status = "trying";
           }

           // If the session exists with active call reject it.
           if (session && !jQuery(session).find(".call").hasClass("inactive")) {
               call.terminate();
               return false;
           }

           // If this is a new session create it
           if (!session) {
               session = GUI.createSession(display_name, uri);
           }

           // Associate the JsSIP Session to the HTML div session
           session.call = call;
           GUI.setCallSessionStatus(session, status);
           jQuery(session).find(".chat input").focus();

           // EVENT CALLBACK DEFINITION

           call.on('connecting', function() {
               // TMP
               if (call.connection.getLocalStreams().length > 0) {
                   window.localStream = call.connection.getLocalStreams()[0];
               }
           });

           // Progress
           call.on('progress',function(e){
               if (e.originator === 'remote') {
                   GUI.setCallSessionStatus(session, 'in-progress');
               }
           });

           // Started
           call.on('accepted',function(e){
               //Attach the streams to the views if it exists.
               if (call.connection.getLocalStreams().length > 0) {
                   localStream = call.connection.getLocalStreams()[0];
                   selfView = JsSIP.rtcninja.attachMediaStream(selfView, localStream);
                   selfView.volume = 0;

                   // TMP
                   window.localStream = localStream;
               }

               if (e.originator === 'remote') {
                   if (e.response.getHeader('X-Can-Renegotiate') === 'false') {
                       call.data.remoteCanRenegotiateRTC = false;
                   }
                   else {
                       call.data.remoteCanRenegotiateRTC = true;
                   }
               }

               GUI.setCallSessionStatus(session, 'answered');
           });

           call.on('addstream', function(e) {
               console.log('Tryit: addstream()');
               remoteStream = e.stream;
               remoteView = JsSIP.rtcninja.attachMediaStream(remoteView, remoteStream);
           });

           // Failed
           call.on('failed',function(e) {
               var
                   cause = e.cause,
                   response = e.response;

               if (e.originator === 'remote' && cause.match("SIP;cause=200", "i")) {
                   cause = 'answered_elsewhere';
               }

               GUI.setCallSessionStatus(session, 'terminated', cause);
               soundPlayer.setAttribute("src", base_url + "embed/sounds/outgoing-call-rejected.wav");
               soundPlayer.play();
               GUI.removeSession(session, 1500);
               selfView.src = '';
               remoteView.src = '';

               _Session = null;
           });

           // NewDTMF
           call.on('newDTMF',function(e) {
               if (e.originator === 'remote') {
                   sound_file = e.dtmf.tone;
                   soundPlayer.setAttribute("src", base_url + "embed/sounds/dialpad/" + sound_file + ".ogg");
                   soundPlayer.play();
               }
           });

           call.on('hold',function(e) {
               soundPlayer.setAttribute("src", base_url + "embed/sounds/dialpad/pound.ogg");
               soundPlayer.play();

               GUI.setCallSessionStatus(session, 'hold', e.originator);
           });

           call.on('unhold',function(e) {
               soundPlayer.setAttribute("src", base_url + "embed/sounds/dialpad/pound.ogg");
               soundPlayer.play();

               GUI.setCallSessionStatus(session, 'unhold', e.originator);
           });

           // Ended
           call.on('ended', function(e) {
               var cause = e.cause;

               GUI.setCallSessionStatus(session, "terminated", cause);
               GUI.removeSession(session, 1500);
               selfView.src = '';
               remoteView.src = '';

               _Session = null;
               JsSIP.rtcninja.closeMediaStream(localStream);
           });

           // received UPDATE
           call.on('update', function(e) {
               var request = e.request;

               if (! request.body) { return; }

               if (! localCanRenegotiateRTC() || ! call.data.remoteCanRenegotiateRTC) {
                   console.warn('Tryit: UPDATE received, resetting PeerConnection');
                   call.connection.reset();
                   call.connection.addStream(localStream);
               }
           });

           // received reINVITE
           call.on('reinvite', function(e) {
               var request = e.request;

               if (! request.body) { return; }

               if (! localCanRenegotiateRTC() || ! call.data.remoteCanRenegotiateRTC) {
                   console.warn('Tryit: reINVITE received, resetting PeerConnection');
                   call.connection.reset();
                   call.connection.addStream(localStream);
               }
           });
       }
       ,
       removeSession : function(session, time, force) {
           var default_time = 500;
           var uri = jQuery(session).find(".peer > .uri").text();
           var chat_input = jQuery(session).find(".chat > input[type='text']");

           //if (force || (jQuery(session).find(".chat .chatting").hasClass("inactive") && (chat_input.hasClass("inactive") || chat_input.val() == ""))) {
               time = ( time ? time : default_time );
               jQuery(session).fadeTo(time, 0.7, function() {
                   jQuery(session).slideUp(100, function() {
                       jQuery(session).remove();
                   });
               });
               // Enviar "iscomposing idle" si estábamos escribiendo.
               if (! chat_input.hasClass("inactive") && chat_input.val() != "")
                   GUI.jssipIsComposing(uri, false);
           /*
           }
           else {
               // Como existe una sesión de chat, no cerramos el div de sesión,
               // en su lugar esperamos un poco antes de ponerlo como "inactive".
               setTimeout('GUI.setDelayedCallSessionStatus("'+uri+'", "inactive")', 1000);
           }
           */
           // hide HTML Video Elements
           //jQuery('#remoteView').attr('hidden', true);
           //jQuery('#selfView').attr('hidden', true);
       }
       ,
       setDelayedCallSessionStatus : function(uri, status, description, force) {
           var session = GUI.getSession(uri.toString());
           if (session)
               GUI.setCallSessionStatus(session, status, description, force);
       }
       ,
       /*
        * - who: "me" o "peer".
        * - text: el texto del mensaje.
        */
       addChatMessage : function(session, who, text) {
           var chatting = jQuery(session).find(".chat > .chatting");
           jQuery(chatting).removeClass("inactive");

           if (who != "error") {
               var who_text = ( who == "me" ? "Me" : jQuery(session).find(".peer > .display-name").text() );
               var message_div = jQuery('<p class="' + who + '"><b>' + who_text + '</b>: ' + text + '</p>');
           }
           // ERROR sending the MESSAGE.
           else {
               var message_div = jQuery('<p class="error"><i>message failed: ' + text + '</i>');
           }
           jQuery(chatting).append(message_div);
           jQuery(chatting).scrollTop(1e4);

           if (who == "peer") {
               soundPlayer.setAttribute("src", base_url + "embed/sounds/incoming-chat.ogg");
               soundPlayer.play();
           }

           // Si se había recibido un iscomposing quitarlo (sólo si es message entrante!!!).
           if (who == "peer")
               jQuery(session).find(".chat .iscomposing").hide();
       }
   }

   // Add/remove video during a call.
   jQuery('#enableVideo').change(function() {
       if (! _Session) { return; }

       if (! _Session.isReadyToReOffer()) {
           console.warn('Tryit: not ready to reoffer');
           return;
       }

       var mediaConstraints = { audio: true, video: true };

       if (! jQuery(this).is(':checked')) {
           mediaConstraints.video = false;
       }

       JsSIP.rtcninja.getUserMedia(mediaConstraints,
           useNewLocalStream,
           function(error) {
               throw error;
           }
       );
       /*
       function useNewLocalStream(stream) {
           if (! _Session) { return; }

           var oldStream = _Session.connection.getLocalStreams()[0];

           _Session.connection.removeStream(oldStream);
           JsSIP.rtcninja.closeMediaStream(oldStream);
           _Session.connection.addStream(stream);
           _Session.renegotiate();
           selfView = JsSIP.rtcninja.attachMediaStream(selfView, stream);
       }
       */

       function useNewLocalStream(stream) {
           if (! _Session) { return; }

           var oldStream = localStream;

           if (localCanRenegotiateRTC() && _Session.data.remoteCanRenegotiateRTC) {
               _Session.connection.removeStream(localStream);
               _Session.connection.addStream(stream);
           }
           else {
               console.warn('Tryit: resetting PeerConnection before renegotiating the session');
               _Session.connection.reset();
               _Session.connection.addStream(stream);
           }

           JsSIP.rtcninja.closeMediaStream(localStream);

           _Session.renegotiate({
               useUpdate: true,
               rtcOfferConstraints: { offerToReceiveAudio: true, offerToReceiveVideo: true }
           });

           localStream = stream;
           selfView = JsSIP.rtcninja.attachMediaStream(selfView, stream);
       }

   });

});

function initPhone(user_name, password, display_name, extension_domain, ws_servers){
   ws_registered = false;
   var sip_password =  password;
   var username = user_name;

   var sip_uri = 'sip:'+username+'@'+extension_domain;
   //alert('Init URI: '+ sip_uri);
   //alert('Init Display: '+ display_name);
   //var sip_uri = 'sip:849389891010001@epacific.net';
   var configuration  = {
       log: { level: 'debug' },
       uri: sip_uri,
       password:  sip_password,
       ws_servers:  ws_servers,
       display_name: display_name
   };

   div_webcam.show();

   try {
       ua = new JsSIP.UA(configuration);
   } catch(e) {
       console.log(e.toString());
       //alert(e.toString());
       return;
   }
   // Call/Message reception callbacks
   ua.on('newRTCSession', function(e) {
       // Set a global '_Session' variable with the session for testing.
       _Session = e.session;
       GUI.newSession(e)
   });

   ua.on('newMessage', function(e) {
       GUI.newMessage(e)
   });
   ua.on('connected', function(e) {
       //document.title = 'Connected';
       //GUI.setStatus("connected");
       // Habilitar el phone.
       //obj.attr('disabled', true);
       ws_was_connected = true;
       //alert('Connected');
   });
   ua.on('disconnected', function(e) {
       //document.title = 'Disconnected';
       //GUI.setStatus("disconnected");

       if (! ws_was_connected) {
           //alert("WS connection error:\n\n- WS close code: " + e.data.code + "\n- WS close reason: " + e.data.reason);
           console.error("WS connection error | WS close code: " + e.code + " | WS close reason: " + e.reason);
           //if (! window.CustomJsSIPSettings) { window.location.reload(false); }
       }
       //alert('Disconnected');
   });
   ua.on('registered', function(e){
       console.info('Registered');
       //GUI.setStatus("Registered");
       //alert('Registered');
       //GUI.jssipCall(target);
       ws_registered = true;
       //jQuery('span.display-name').html(configuration.display_name);
       //jQuery('span.uri').html(configuration.uri);
   });

   ua.on('unregistered', function(e){
       console.info('Unregistered');
       GUI.setStatus("Unregistered");
       ws_registered = false;
       //alert('Unregistered');
   });

   ua.on('registrationFailed', function(e) {
       console.info('Registration failure');
       ws_registered = false;
       //alert('Register Fail!');
       if (! e.response) {
           // alert("SIP registration error:\n" + e.data.cause);
       }
       else {
           // alert("SIP registration error:\n" + e.data.response.status_code.toString() + " " + e.data.response.reason_phrase)
       }
       // if (! window.CustomJsSIPSettings) { window.location.reload(false); }
   });

   // Start
   ua.start();
   //ua.call('800@');
}

var timer;
function waitAndCall(){
   timer = setTimeout("waitAndCall()", 1000);
   if(ws_registered){
       if(timer) clearTimeout(timer);
       //alert(main_phone);
       GUI.jssipCall(main_phone);
   }
}

Hãy liên lạc với chúng tôi để được hỗ trợ kịp thời, qua: