/** * @copyright 2021 Double Bastion LLC * * @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 . * * * * 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(' '+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')? '': 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 = '
'; i += '
'; i += ' '+ctxSip.formatPhone(item.uri)+'
'+moment(item.start).format('MM/DD hh:mm:ss a')+''; i += '
'; i += '
'+item.clid+'
' + callLength+'
'; if (callActive) { i += '
'; if (item.status === 'ringing' && item.flow === 'incoming') { i += ''; } else { i += ''; i += ''; i += ''; } i += ''; i += '
'; } i += '
'; $('#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('Transfering the call...'); 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 = ''; $("#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; } }; });