phone/scripts/app.js
65165a7c
 /**
  * @copyright 2021 Double Bastion LLC <www.doublebastion.com>
  *
  * @author Double Bastion LLC
  *
  * @license GNU AGPL version 3 or any later version
  *
  * This program is free software; you can redistribute it and/or
  * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
  * License as published by the Free Software Foundation; either
  * version 3 of the License, or any later version.
  *
  * This program is distributed in the hope that it will be useful,
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
  *
  * You should have received a copy of the GNU Affero General Public
  * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
  *
  *
  *
  * This is a modified version of the original file "app.js".
  *
  * We list below the copyright notice of the ctxSip phone (https://github.com/collecttix/ctxSip)
  * which also applies to the original "app.js" file, which was part of it:
  *
  *
  *  The MIT License (MIT)
  *
  *  Copyright (c) 2014 Collecttix
  *
  *  Permission is hereby granted, free of charge, to any person obtaining a copy
  *  of this software and associated documentation files (the "Software"), to deal
  *  in the Software without restriction, including without limitation the rights
  *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  *  copies of the Software, and to permit persons to whom the Software is
  *  furnished to do so, subject to the following conditions:
  *
  *  The above copyright notice and this permission notice shall be included in
  *  all copies or substantial portions of the Software.
  *
  *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  *  THE SOFTWARE.
  *
  */
 
 
 /* globals SIP, user, moment, Stopwatch */
 
 $(document).ready(function() {
 
     var ctxSip;
 
     // Show system notifications on incoming calls
     function incomingCallNote() {
        var noticeOptions = { body: "New incoming call !!!", icon: "images/sip_trip_phone_logo.svg" }
        var inComingCallNotification = new Notification("SIP Trip Phone incoming call", noticeOptions);
        inComingCallNotification.onclick = function (event) {
          return;
        }
 
        if (document.hasFocus()) {
            return;
        } else { setTimeout(incomingCallNote, 8000); }
     }
 
     // Change page title on incoming calls
     function changePageTitle() {
         if ($(document).attr("title") == "SIP Trip Phone") { $(document).prop("title", "New call !!!"); } else { $(document).prop("title", "SIP Trip Phone"); }
         if (document.hasFocus()) {
             $(document).prop("title", "SIP Trip Phone");
             return;
         } else { setTimeout(changePageTitle, 460); }
     }
 
     var userSIPPass = window.opener.sipUserPasswd;
 
     var user = JSON.parse(localStorage.getItem('SIPCreds'));
 
     if (user.Stun != '') {
         var configComp = {
                password        : userSIPPass,
                displayName     : user.Display,
                uri             : 'sip:'+user.User+'@'+user.Realm,
                wsServers       : user.WSServer,
                registerExpires : 9999999,
                traceSip        : true,
                stunServers: ["stun:" + user.Stun],
                log             : {
                    level : 0,
                }
             };
     } else {
         var configComp = {
                password        : userSIPPass,
                displayName     : user.Display,
                uri             : 'sip:'+user.User+'@'+user.Realm,
                wsServers       : user.WSServer,
                registerExpires : 9999999,
                traceSip        : true,
                log             : {
                    level : 0,
                }
             };
     }
 
     ctxSip = {
 
         config : configComp,
         ringtone     : document.getElementById('ringtone'),
         ringbacktone : document.getElementById('ringbacktone'),
         dtmfTone     : document.getElementById('dtmfTone'),
 
         Sessions     : [],
         callTimers   : {},
         callActiveID : null,
         callVolume   : 1,
         Stream       : null,
 
         /**
          * Parses a SIP uri and returns a formatted phone number.
          *
          * @param  {string} phone number or uri to format
          * @return {string}       formatted number
          */
         formatPhone : function(phone) {
 
             var num;
 
             if (phone.indexOf('@')) {
                 num =  phone.split('@')[0];
             } else {
                 num = phone;
             }
 
             num = num.toString().replace(/[^0-9]/g, '');
 
             if (num.length === 10) {
                 return '(' + num.substr(0, 3) + ') ' + num.substr(3, 3) + '-' + num.substr(6,4);
             } else if (num.length === 11) {
                 return '(' + num.substr(1, 3) + ') ' + num.substr(4, 3) + '-' + num.substr(7,4);
             } else {
                 return num;
             }
         },
 
         // Sound methods
         startRingTone : function() {
             try { ctxSip.ringtone.play(); } catch (e) { }
         },
 
         stopRingTone : function() {
             try { ctxSip.ringtone.pause(); } catch (e) { }
         },
 
         startRingbackTone : function() {
             try { ctxSip.ringbacktone.play(); } catch (e) { }
         },
 
         stopRingbackTone : function() {
             try { ctxSip.ringbacktone.pause(); } catch (e) { }
         },
 
         // Genereates a rendom string to ID a call
         getUniqueID : function() {
             return Math.random().toString(36).substr(2, 9);
         },
 
 
         newSession : function(newSess) {
 
             newSess.displayName = newSess.remoteIdentity.displayName || newSess.remoteIdentity.uri.user;
             newSess.ctxid       = ctxSip.getUniqueID();
 
             var status;
 
             if (newSess.direction === 'incoming') {
                 status = "Incoming: "+ newSess.displayName;
                 ctxSip.startRingTone();
 
                 incomingCallNote();
                 changePageTitle();
 
             } else {
                 status = "Trying: "+ newSess.displayName;
                 ctxSip.startRingbackTone();
             }
 
             ctxSip.logCall(newSess, 'ringing');
 
             ctxSip.setCallSessionStatus(status);
 
             // EVENT CALLBACKS
 
             newSess.on('progress',function(e) {
                 if (e.direction === 'outgoing') {
                     ctxSip.setCallSessionStatus('Calling...');
                 }
             });
 
             newSess.on('connecting',function(e) {
                 if (e.direction === 'outgoing') {
                     ctxSip.setCallSessionStatus('Connecting...');
                 }
             });
 
 
            newSess.on('accepted',function(e) {
 
              // If there is another active call, hold it
              if (ctxSip.callActiveID && ctxSip.callActiveID !== newSess.ctxid) {
                  ctxSip.phoneHoldButtonPressed(ctxSip.callActiveID);
              }
 
              ctxSip.stopRingbackTone();
              ctxSip.stopRingTone();
              ctxSip.setCallSessionStatus('Answered');
              ctxSip.logCall(newSess, 'answered');
              ctxSip.callActiveID = newSess.ctxid;
            });
 
             newSess.on('hold', function(e) {
                 ctxSip.callActiveID = null;
                 ctxSip.logCall(newSess, 'holding');
             });
 
             newSess.on('unhold', function(e) {
                 ctxSip.logCall(newSess, 'resumed');
                 ctxSip.callActiveID = newSess.ctxid;
             });
 
             newSess.on('muted', function(e) {
                 ctxSip.Sessions[newSess.ctxid].isMuted = true;
                 ctxSip.setCallSessionStatus("Muted");
             });
 
             newSess.on('unmuted', function(e) {
                 ctxSip.Sessions[newSess.ctxid].isMuted = false;
                 ctxSip.setCallSessionStatus("Answered");
             });
 
             newSess.on('cancel', function(e) {
                 ctxSip.stopRingTone();
                 ctxSip.stopRingbackTone();
                 ctxSip.setCallSessionStatus("Canceled");
                 if (this.direction === 'outgoing') {
                     ctxSip.callActiveID = null;
                     newSess             = null;
                     ctxSip.logCall(this, 'ended');
                 }
             });
 
             newSess.on('bye', function(e) {
                 ctxSip.stopRingTone();
                 ctxSip.stopRingbackTone();
                 ctxSip.setCallSessionStatus("");
                 ctxSip.logCall(newSess, 'ended');
                 ctxSip.callActiveID = null;
                 newSess             = null;
             });
 
             newSess.on('failed',function(e) {
                 ctxSip.stopRingTone();
                 ctxSip.stopRingbackTone();
                 ctxSip.setCallSessionStatus('Terminated');
             });
 
             newSess.on('rejected',function(e) {
                 ctxSip.stopRingTone();
                 ctxSip.stopRingbackTone();
                 ctxSip.setCallSessionStatus('Rejected');
                 ctxSip.callActiveID = null;
                 ctxSip.logCall(this, 'ended');
                 newSess             = null;
             });
 
             ctxSip.Sessions[newSess.ctxid] = newSess;
 
         },
 
         // getUser media request refused or device was not present
         getUserMediaFailure : function(e) {
             window.console.error('getUserMedia failed:', e);
             ctxSip.setError(true, 'Media Error.', 'You must allow access to your microphone.  Check the address bar.', true);
         },
 
 
         getUserMediaSuccess : function(stream) {
             ctxSip.Stream = stream;
         },
 
 
         /**
          * sets the ui call status field
          *
          * @param {string} status
          */
         setCallSessionStatus : function(status) {
             $('#txtCallStatus').html(status);
         },
 
         /**
          * sets the ui connection status field
          *
          * @param {string} status
          */
         setStatus : function(status) {
             $("#txtRegStatus").html('<i class="fa fa-signal"></i> '+status);
         },
 
         /**
          * logs a call to localstorage
          *
          * @param  {object} session
          * @param  {string} status Enum 'ringing', 'answered', 'ended', 'holding', 'resumed'
          */
         logCall : function(session, status) {
 
             var log = {
                     clid : session.displayName,
                     uri  : session.remoteIdentity.uri.toString(),
                     id   : session.ctxid,
                     time : new Date().getTime()
                 },
                 calllog = JSON.parse(localStorage.getItem('sipCalls'));
 
             if (!calllog) { calllog = {}; }
 
             if (!calllog.hasOwnProperty(session.ctxid)) {
                 calllog[log.id] = {
                     id    : log.id,
                     clid  : log.clid,
                     uri   : log.uri,
                     start : log.time,
                     flow  : session.direction
                 };
             }
 
             if (status === 'ended') {
                 calllog[log.id].stop = log.time;
             }
 
             if (status === 'ended' && calllog[log.id].status === 'ringing') {
                 calllog[log.id].status = 'missed';
             } else {
                 calllog[log.id].status = status;
             }
 
             localStorage.setItem('sipCalls', JSON.stringify(calllog));
             ctxSip.logShow();
         },
 
         /**
          * adds a ui item to the call log
          *
          * @param  {object} item log item
          */
         logItem : function(item) {
 
             var callActive = (item.status !== 'ended' && item.status !== 'missed'),
                 callLength = (item.status !== 'ended')? '<span id="'+item.id+'"></span>': moment.duration(item.stop - item.start).humanize(),
                 callClass  = '',
                 callIcon,
                 i;
 
             switch (item.status) {
                 case 'ringing'  :
                     callClass = 'list-group-item-success';
                     callIcon  = 'fa-bell';
                     break;
 
                 case 'missed'   :
                     callClass = 'list-group-item-danger';
                     if (item.flow === "incoming") { callIcon = 'fa-chevron-left'; }
                     if (item.flow === "outgoing") { callIcon = 'fa-chevron-right'; }
                     break;
 
                 case 'holding'  :
                     callClass = 'list-group-item-warning';
                     callIcon  = 'fa-pause';
                     break;
 
                 case 'answered' :
                 case 'resumed'  :
                     callClass = 'list-group-item-info';
                     callIcon  = 'fa-phone-square';
                     break;
 
                 case 'ended'  :
                     if (item.flow === "incoming") { callIcon = 'fa-chevron-left'; }
                     if (item.flow === "outgoing") { callIcon = 'fa-chevron-right'; }
                     break;
             }
 
 
             i  = '<div class="list-group-item sip-logitem clearfix '+callClass+'" data-uri="'+item.uri+'" data-sessionid="'+item.id+'" title="Double Click to Call">';
             i += '<div class="clearfix"><div class="pull-left">';
             i += '<i class="fa fa-fw '+callIcon+' fa-fw"></i> <strong>'+ctxSip.formatPhone(item.uri)+'</strong><br><small>'+moment(item.start).format('MM/DD hh:mm:ss a')+'</small>';
             i += '</div>';
             i += '<div class="pull-right text-right"><em>'+item.clid+'</em><br>' + callLength+'</div></div>';
 
             if (callActive) {
                 i += '<div class="btn-group btn-group-xs pull-right">';
                 if (item.status === 'ringing' && item.flow === 'incoming') {
                     i += '<button class="btn btn-xs btn-success btnCall" title="Call"><i class="fa fa-phone"></i></button>';
                 } else {
                     i += '<button class="btn btn-xs btn-primary btnHoldResume" title="Hold"><i class="fa fa-pause"></i></button>';
                     i += '<button class="btn btn-xs btn-info btnTransfer" title="Transfer"><i class="fa fa-random"></i></button>';
                     i += '<button class="btn btn-xs btn-warning btnMute" title="Mute"><i class="fa fa-fw fa-microphone"></i></button>';
                 }
                 i += '<button class="btn btn-xs btn-danger btnHangUp" title="Hangup"><i class="fa fa-stop"></i></button>';
                 i += '</div>';
             }
             i += '</div>';
 
             $('#sip-logitems').append(i);
 
 
             // Start call timer on answer
             if (item.status === 'answered') {
                 var tEle = document.getElementById(item.id);
                 ctxSip.callTimers[item.id] = new Stopwatch(tEle);
                 ctxSip.callTimers[item.id].start();
             }
 
             if (callActive && item.status !== 'ringing') {
                 ctxSip.callTimers[item.id].start({startTime : item.start});
             }
 
             $('#sip-logitems').scrollTop(0);
         },
 
         /**
          * updates the call log ui
          */
         logShow : function() {
 
             var calllog = JSON.parse(localStorage.getItem('sipCalls')),
             x = [];
 
             if (calllog !== null) {
 
                 $('#sip-splash').addClass('hide');
                 $('#sip-log').removeClass('hide');
 
                 // empty existing logs
                 $('#sip-logitems').empty();
 
                 // JS doesn't guarantee property order so
                 // create an array with the start time as
                 // the key and sort by that.
 
                 // Add start time to array
                 $.each(calllog, function(k,v) {
                     x.push(v);
                 });
 
                 // sort descending
                 x.sort(function(a, b) {
                     return b.start - a.start;
                 });
 
                 $.each(x, function(k, v) {
                     ctxSip.logItem(v);
                 });
 
             } else {
                 $('#sip-splash').removeClass('hide');
                 $('#sip-log').addClass('hide');
             }
         },
 
         /**
          * removes log items from localstorage and updates the UI
          */
         logClear : function() {
 
             localStorage.removeItem('sipCalls');
             ctxSip.logShow();
         },
 
         sipCall : function(target) {
 
             try {
                 var s = ctxSip.phone.invite(target, {
                     media : {
                         stream      : ctxSip.Stream,
                         constraints : { audio : true, video : false },
                         render      : { remote: document.getElementById('audioRemote') }
                         // render: { remote: $('#audioRemote').get()[0] }
                         // RTCConstraints : { "optional": [{ 'DtlsSrtpKeyAgreement': 'true'} ]}
                     }
                 });
                 s.direction = 'outgoing';
                 ctxSip.newSession(s);
 
             } catch(e) {
                 throw(e);
             }
         },
 
         sipTransfer : function(sessionid) {
 
                 var s  = ctxSip.Sessions[sessionid],
                 target = window.prompt('Enter destination number', '');
 
             ctxSip.setCallSessionStatus('<i>Transfering the call...</i>');
             s.refer(target);
         },
 
         sipHangUp : function(sessionid) {
 
             var s = ctxSip.Sessions[sessionid];
             // s.terminate();
             if (!s) {
                 return;
             } else if (s.startTime) {
                 s.bye();
             } else if (s.reject) {
                 s.reject();
             } else if (s.cancel) {
                 s.cancel();
             }
 
         },
 
         sipSendDTMF : function(digit) {
 
             try { ctxSip.dtmfTone.play(); } catch(e) { }
 
             var a = ctxSip.callActiveID;
             if (a) {
                 var s = ctxSip.Sessions[a];
                 s.dtmf(digit);
             }
         },
 
         phoneCallButtonPressed : function(sessionid) {
 
                 var s  = ctxSip.Sessions[sessionid],
                 target = $("#numDisplay").val();
 
             if (!s) {
 
                 $("#numDisplay").val("");
                 ctxSip.sipCall(target);
 
             } else if (s.accept && !s.startTime) {
 
                 s.accept({
                     media: {
                             stream: ctxSip.Stream,
                             constraints: { audio: true, video: false },
                             render      : { remote: document.getElementById('audioRemote') }
                             // render: { remote: $('#audioRemote').get()[0] }
                             // RTCConstraints : { "optional": [{ 'DtlsSrtpKeyAgreement': 'true'} ]}
                            }
                 });
             }
         },
 
         phoneMuteButtonPressed : function (sessionid) {
 
             var s = ctxSip.Sessions[sessionid];
 
             if (!s.isMuted) {
                 s.mute();
             } else {
                 s.unmute();
             }
         },
 
         phoneHoldButtonPressed : function(sessionid) {
 
             var s = ctxSip.Sessions[sessionid];
 
             if (s.isOnHold().local === true) {
                 s.unhold();
             } else {
                 s.hold();
             }
         },
 
 
         setError : function(err, title, msg, closable) {
 
             // Show modal if err = true
             if (err === true) {
                 $("#mdlError p").html(msg);
                 $("#mdlError").modal('show');
 
                 if (closable) {
                     var b = '<button type="button" class="close" data-dismiss="modal">&times;</button>';
                     $("#mdlError .modal-header").find('button').remove();
                     $("#mdlError .modal-header").prepend(b);
                     $("#mdlError .modal-title").html(title);
                     $("#mdlError").modal({ keyboard : true });
                 } else {
                     $("#mdlError .modal-header").find('button').remove();
                     $("#mdlError .modal-title").html(title);
                     $("#mdlError").modal({ keyboard : false });
                 }
                 $('#numDisplay').prop('disabled', 'disabled');
             } else {
                 $('#numDisplay').removeProp('disabled');
                 $("#mdlError").modal('hide');
             }
         },
 
         /**
          * Tests for a capable browser, return bool, and shows an
          * error modal on fail.
          */
         hasWebRTC : function() {
 
             if (navigator.webkitGetUserMedia) {
                 return true;
             } else if (navigator.mozGetUserMedia) {
                 return true;
             } else if (navigator.getUserMedia) {
                 return true;
             } else {
                 ctxSip.setError(true, 'Unsupported Browser.', 'Your browser does not support the features required for this phone.');
                 window.console.error("WebRTC support not found");
                 return false;
             }
         }
     };
 
     userSIPPass = '';
     window.opener.sipUserPasswd = '';
 
     // Throw an error if the browser can't hack it.
     if (!ctxSip.hasWebRTC()) {
         return true;
     }
 
     ctxSip.phone = new SIP.UA(ctxSip.config);
 
     ctxSip.phone.on('connected', function(e) {
         ctxSip.setStatus("Connected");
     });
 
     ctxSip.phone.on('disconnected', function(e) {
         ctxSip.setStatus("Disconnected");
 
         // disable phone
         ctxSip.setError(true, 'Websocket Disconnected.', 'An Error occurred connecting to the websocket.');
 
         // remove existing sessions
         $("#sessions > .session").each(function(i, session) {
             ctxSip.removeSession(session, 500);
         });
     });
 
     ctxSip.phone.on('registered', function(e) {
 
         var closeEditorWarning = function() {
             return 'If you close this window, you will not be able to make or receive calls from your browser.';
         };
 
         var closePhone = function() {
             // stop the phone on unload
             localStorage.removeItem('SipTripPhone');
             ctxSip.phone.stop();
         };
 
         window.onbeforeunload = closeEditorWarning;
         window.onunload       = closePhone;
 
         // This key is set to prevent multiple windows.
         localStorage.setItem('SipTripPhone', 'true');
 
         $("#mldError").modal('hide');
         ctxSip.setStatus("Ready");
 
         // Get the userMedia and cache the stream
         var audio = document.getElementById('audioRemote');
         var mediaStream = new MediaStream();
         let audioTrack = null;
 
         navigator.mediaDevices.getUserMedia({ audio : true, video : false }, ctxSip.getUserMediaSuccess, ctxSip.getUserMediaFailure).then(function(mediaStream) {
 
            let audioTracks = mediaStream.getAudioTracks();
            audio.srcObject = mediaStream;
 
            if (audioTracks.length) {
                audioTrack = audioTracks[0];
            }
         }).then(function() {
            new Promise(function(resolve) {
                audio.onloadedmetadata = resolve;
            })
         })
 
     });
 
     ctxSip.phone.on('registrationFailed', function(e) {
         ctxSip.setError(true, 'Registration Error.', 'An Error occurred registering your phone. Check your settings.');
         ctxSip.setStatus("Error: Registration Failed");
     });
 
     ctxSip.phone.on('unregistered', function(e) {
         ctxSip.setError(true, 'Registration Error.', 'An Error occurred registering your phone. Check your settings.');
         ctxSip.setStatus("Error: Registration Failed");
     });
 
     ctxSip.phone.on('invite', function (incomingSession) {
 
         var s = incomingSession;
 
         s.direction = 'incoming';
         ctxSip.newSession(s);
     });
 
     // Auto-focus number input on backspace.
     $('#sipClient').keydown(function(event) {
         if (event.which === 8) {
             $('#numDisplay').focus();
         }
     });
 
     $('#numDisplay').keypress(function(e) {
         // Enter pressed? so Dial.
         if (e.which === 13) {
             ctxSip.phoneCallButtonPressed();
         }
     });
 
     var clck = 0;
 
     $('.digit').click(function(event) {
 
      if (event.shiftKey) {
 
          clck++;
          event.preventDefault();
          var num = $('#numDisplay').val();
          var dig;
          var diginit = $(this).data('digit').toString().split(',');
          var elct = diginit.length;
 
          dig = diginit[clck%elct];
          var numsec = num.slice(0,-1);
          $('#numDisplay').val(numsec+dig);
          ctxSip.sipSendDTMF(dig);
 
      } else {
          event.preventDefault();
          var num = $('#numDisplay').val();
          var dig;
          var diginit = $(this).data('digit').toString().split(',');
 
          dig = diginit[0];
          clck = 0;
          $('#numDisplay').val(num+dig);
          ctxSip.sipSendDTMF(dig);
        }
 
        return false;
 
     });
 
     $('#phoneUI .dropdown-menu').click(function(e) {
         e.preventDefault();
     });
 
     $('#phoneUI').delegate('.btnCall', 'click', function(event) {
         ctxSip.phoneCallButtonPressed();
         // to close the dropdown
         return true;
     });
 
     $('.sipLogClear').click(function(event) {
         event.preventDefault();
         ctxSip.logClear();
     });
 
     $('#sip-logitems').delegate('.sip-logitem .btnCall', 'click', function(event) {
         var sessionid = $(this).closest('.sip-logitem').data('sessionid');
         ctxSip.phoneCallButtonPressed(sessionid);
         return false;
     });
 
     $('#sip-logitems').delegate('.sip-logitem .btnHoldResume', 'click', function(event) {
         var sessionid = $(this).closest('.sip-logitem').data('sessionid');
         ctxSip.phoneHoldButtonPressed(sessionid);
         return false;
     });
 
     $('#sip-logitems').delegate('.sip-logitem .btnHangUp', 'click', function(event) {
         var sessionid = $(this).closest('.sip-logitem').data('sessionid');
         ctxSip.sipHangUp(sessionid);
         return false;
     });
 
     $('#sip-logitems').delegate('.sip-logitem .btnTransfer', 'click', function(event) {
         var sessionid = $(this).closest('.sip-logitem').data('sessionid');
         ctxSip.sipTransfer(sessionid);
         return false;
     });
 
     $('#sip-logitems').delegate('.sip-logitem .btnMute', 'click', function(event) {
         var sessionid = $(this).closest('.sip-logitem').data('sessionid');
         ctxSip.phoneMuteButtonPressed(sessionid);
         return false;
     });
 
     $('#sip-logitems').delegate('.sip-logitem', 'dblclick', function(event) {
         event.preventDefault();
 
         var uri = $(this).data('uri');
         $('#numDisplay').val(uri);
         ctxSip.phoneCallButtonPressed();
     });
 
     $('#sldVolume').on('change', function() {
 
             var v  = $(this).val() / 100,
             btn    = $('#btnVol'),
             icon   = $('#btnVol').find('i'),
             active = ctxSip.callActiveID;
 
         // Set the object and media stream volumes
         if (ctxSip.Sessions[active]) {
             ctxSip.Sessions[active].player.volume = v;
             ctxSip.callVolume                     = v;
         }
 
         // Set the others
         $('audio').each(function() {
             $(this).get()[0].volume = v;
         });
 
         if (v < 0.1) {
             btn.removeClass(function (index, css) {
                    return (css.match (/(^|\s)btn\S+/g) || []).join(' ');
                 })
                 .addClass('btn btn-sm btn-danger');
             icon.removeClass().addClass('fa fa-fw fa-volume-off');
         } else if (v < 0.8) {
             btn.removeClass(function (index, css) {
                    return (css.match (/(^|\s)btn\S+/g) || []).join(' ');
                }).addClass('btn btn-sm btn-info');
             icon.removeClass().addClass('fa fa-fw fa-volume-down');
         } else {
             btn.removeClass(function (index, css) {
                    return (css.match (/(^|\s)btn\S+/g) || []).join(' ');
                }).addClass('btn btn-sm btn-primary');
             icon.removeClass().addClass('fa fa-fw fa-volume-up');
         }
         return false;
     });
 
     // Hide the spalsh after 3 secs.
     setTimeout(function() {
         ctxSip.logShow();
     }, 3000);
 
 
     /**
      * Stopwatch object used for call timers
      *
      * @param {dom element} elem
      * @param {[object]} options
      */
     var Stopwatch = function(elem, options) {
 
         // private functions
         function createTimer() {
             return document.createElement("span");
         }
 
         var timer = createTimer(),
             offset,
             clock,
             interval;
 
         // default options
         options           = options || {};
         options.delay     = options.delay || 1000;
         options.startTime = options.startTime || Date.now();
 
         // append elements
         elem.appendChild(timer);
 
         function start() {
             if (!interval) {
                 offset   = options.startTime;
                 interval = setInterval(update, options.delay);
             }
         }
 
         function stop() {
             if (interval) {
                 clearInterval(interval);
                 interval = null;
             }
         }
 
         function reset() {
             clock = 0;
             render();
         }
 
         function update() {
             clock += delta();
             render();
         }
 
         function render() {
             timer.innerHTML = moment(clock).format('mm:ss');
         }
 
         function delta() {
             var now = Date.now(),
                 d   = now - offset;
 
             offset = now;
             return d;
         }
 
         // initialize
         reset();
 
         // public API
         this.start = start; //function() { start; }
         this.stop  = stop; //function() { stop; }
     };
 
 });