Browse code

added appinfo/signature.json CHANGELOG.txt phone/index.html phone/scripts/app.js phone/css/ctxSip.css js/launchphone.js

DoubleBastionAdmin authored on 09/01/2024 22:37:41
Showing 1 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,1015 @@
1
+/**
2
+ * @copyright 2021 Double Bastion LLC <www.doublebastion.com>
3
+ *
4
+ * @author Double Bastion LLC
5
+ *
6
+ * @license GNU AGPL version 3 or any later version
7
+ *
8
+ * This program is free software; you can redistribute it and/or
9
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
10
+ * License as published by the Free Software Foundation; either
11
+ * version 3 of the License, or any later version.
12
+ *
13
+ * This program is distributed in the hope that it will be useful,
14
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
17
+ *
18
+ * You should have received a copy of the GNU Affero General Public
19
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
20
+ *
21
+ *
22
+ *
23
+ * This is a modified version of the original file "app.js".
24
+ *
25
+ * Below is the copyright notice of ctxSip phone (https://github.com/collecttix/ctxSip)
26
+ * which also applies to the original "app.js" file, which was part of ctxSip phone:
27
+ *
28
+ *
29
+ *  The MIT License (MIT)
30
+ *
31
+ *  Copyright (c) 2014 Collecttix
32
+ *
33
+ *  Permission is hereby granted, free of charge, to any person obtaining a copy
34
+ *  of this software and associated documentation files (the "Software"), to deal
35
+ *  in the Software without restriction, including without limitation the rights
36
+ *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
37
+ *  copies of the Software, and to permit persons to whom the Software is
38
+ *  furnished to do so, subject to the following conditions:
39
+ *
40
+ *  The above copyright notice and this permission notice shall be included in
41
+ *  all copies or substantial portions of the Software.
42
+ *
43
+ *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
44
+ *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
45
+ *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
46
+ *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
47
+ *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
48
+ *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
49
+ *  THE SOFTWARE.
50
+ *
51
+ */
52
+
53
+
54
+/* globals SIP, user, moment, Stopwatch */
55
+
56
+$(document).ready(function() {
57
+
58
+    var ctxSip;
59
+
60
+    // Show system notifications on incoming calls
61
+    function incomingCallNote() {
62
+       var noticeOptions = { body: "New incoming call !!!", icon: "images/sip_trip_phone_logo.svg" }
63
+       var inComingCallNotification = new Notification("SIP Trip Phone incoming call", noticeOptions);
64
+       inComingCallNotification.onclick = function (event) {
65
+         return;
66
+       }
67
+
68
+       if (document.hasFocus()) {
69
+           return;
70
+       } else { setTimeout(incomingCallNote, 8000); }
71
+    }
72
+
73
+    // Change page title on incoming calls
74
+    function changePageTitle() {
75
+        if ($(document).attr("title") == "SIP Trip Phone") { $(document).prop("title", "New call !!!"); } else { $(document).prop("title", "SIP Trip Phone"); }
76
+        if (document.hasFocus()) {
77
+            $(document).prop("title", "SIP Trip Phone");
78
+            return;
79
+        } else { setTimeout(changePageTitle, 460); }
80
+    }
81
+
82
+    var userSIPPass = window.opener.sipUserPasswd;
83
+
84
+    var user = JSON.parse(localStorage.getItem('SIPCreds'));
85
+
86
+    var traceornot = (user.Tracesipmsg == 1)? true : false;
87
+
88
+    // Add the 'available phone numbers' drop-down list
89
+    var stpVoiceNmbrs = user.Voicenumbers;
90
+    var stpVoiceNbOpt = '';
91
+    var stpVoiceNmbrsArr = [];
92
+
93
+    if (stpVoiceNmbrs != '') {
94
+
95
+	stpVoiceNmbrsArr = stpVoiceNmbrs.split(",");
96
+	var stpDefNmbrFdb = user.Defaultvoicenumber;
97
+
98
+	for (var v = 0; v < stpVoiceNmbrsArr.length; v++) {
99
+             stpVoiceNbOpt += '<option value="'+ stpVoiceNmbrsArr[v] +'" class="stpSlctFrmNmbrs">'+ stpVoiceNmbrsArr[v] +'</option>';
100
+	}
101
+    }
102
+    stpVoiceNbOpt += '<option value="'+ user.User +'" class="stpSlctFrmNmbrs">'+ user.User +'</option>';
103
+
104
+    $("#fromNumber").append(stpVoiceNbOpt);
105
+
106
+    if (stpDefNmbrFdb != '' && stpDefNmbrFdb != null) {
107
+        $("#fromNumber").val(stpDefNmbrFdb);
108
+    } else {
109
+        if (stpVoiceNmbrsArr.length > 0) {
110
+            $("#fromNumber").val(stpVoiceNmbrsArr[0]);
111
+        } else { $("#fromNumber").val(user.User); }
112
+    }
113
+
114
+    // Adjust height of text frame and call history list and width of dial pad
115
+    $("#sip-splash").css({ "height" : window.innerHeight - 134 });
116
+    $("#sip-logitems").css({ "height" : window.innerHeight - 174 });
117
+
118
+    $(window).resize(function() {
119
+      $("#sip-splash").css({ "height" : window.innerHeight - 134 });
120
+      $("#sip-logitems").css({ "height" : window.innerHeight - 174 });
121
+    });
122
+
123
+    var calcWidth = parseInt(window.innerWidth - 30) + "px";
124
+    var calcHeight = parseInt(window.innerHeight - 134) + "px";
125
+    $("#sip-dialpad").css({ "width" : calcWidth, "height" : calcHeight });
126
+
127
+    var calcMargin =  parseInt(($("#sip-dialpad").height() - $("#dialpadWrap").height()) / 2)  + "px auto";
128
+    $("#dialpadWrap").css("margin", calcMargin);
129
+
130
+
131
+    if (user.Stun != '') {
132
+        var configComp = {
133
+               password        : userSIPPass,
134
+               displayName     : user.Display,
135
+               uri             : 'sip:'+ user.User +'@'+ user.Realm,
136
+               wsServers       : user.WSServer,
137
+               stunServers     : ["stun:"+ user.Stun],
138
+               traceSip        : traceornot,
139
+               log             : { level : 3 },
140
+               registerExpires : 9999999
141
+            };
142
+    } else {
143
+        var configComp = {
144
+               password        : userSIPPass,
145
+               displayName     : user.Display,
146
+               uri             : 'sip:'+ user.User +'@'+ user.Realm,
147
+               wsServers       : user.WSServer,
148
+               traceSip        : traceornot,
149
+               log             : { level : 3 },
150
+               registerExpires : 9999999
151
+            };
152
+    }
153
+
154
+    ctxSip = {
155
+
156
+        config : configComp,
157
+        ringtone     : document.getElementById('ringtone'),
158
+        ringbacktone : document.getElementById('ringbacktone'),
159
+        // dtmfTone     : document.getElementById('dtmfTone'),
160
+
161
+        Sessions     : [],
162
+        callTimers   : {},
163
+        callActiveID : null,
164
+        callVolume   : 1,
165
+        Stream       : null,
166
+
167
+        /**
168
+         * Parses a SIP uri and returns a formatted phone number.
169
+         *
170
+         * @param  {string} phone number or uri to format
171
+         * @return {string}       formatted number
172
+         */
173
+        formatPhone : function(phone) {
174
+
175
+            var num;
176
+
177
+            if (phone.indexOf('@')) {
178
+                num =  phone.split('@')[0];
179
+            } else {
180
+                num = phone;
181
+            }
182
+
183
+            num = num.toString().replace(/[^0-9]/g, '');
184
+
185
+            if (num.length === 10) {
186
+                return '(' + num.substr(0, 3) + ') ' + num.substr(3, 3) + '-' + num.substr(6,4);
187
+            } else if (num.length === 11) {
188
+                return '(' + num.substr(1, 3) + ') ' + num.substr(4, 3) + '-' + num.substr(7,4);
189
+            } else {
190
+                return num;
191
+            }
192
+        },
193
+
194
+        // Sound methods
195
+        startRingTone : function() {
196
+            try { ctxSip.ringtone.play(); } catch (e) { }
197
+        },
198
+
199
+        stopRingTone : function() {
200
+            try { ctxSip.ringtone.pause(); } catch (e) { }
201
+        },
202
+
203
+        startRingbackTone : function() {
204
+            try { ctxSip.ringbacktone.play(); } catch (e) { }
205
+        },
206
+
207
+        stopRingbackTone : function() {
208
+            try { ctxSip.ringbacktone.pause(); } catch (e) { }
209
+        },
210
+
211
+        // Genereates a random string to ID a call
212
+        getUniqueID : function() {
213
+            return Math.random().toString(36).substr(2, 9);
214
+        },
215
+
216
+
217
+        newSession : function(newSess) {
218
+
219
+            newSess.displayName = newSess.remoteIdentity.displayName || newSess.remoteIdentity.uri.user;
220
+            newSess.ctxid       = ctxSip.getUniqueID();
221
+
222
+            var status;
223
+
224
+            if (newSess.direction === 'incoming') {
225
+                status = "Incoming: "+ newSess.displayName;
226
+                ctxSip.startRingTone();
227
+
228
+                incomingCallNote();
229
+                changePageTitle();
230
+
231
+            } else {
232
+                status = "Trying: "+ newSess.displayName;
233
+                ctxSip.startRingbackTone();
234
+            }
235
+
236
+            ctxSip.logCall(newSess, 'ringing');
237
+
238
+            ctxSip.setCallSessionStatus(status);
239
+
240
+            // EVENT CALLBACKS
241
+
242
+            newSess.on('progress',function(e) {
243
+                if (e.direction === 'outgoing') {
244
+                    ctxSip.setCallSessionStatus('Calling...');
245
+                }
246
+            });
247
+
248
+            newSess.on('connecting',function(e) {
249
+                if (e.direction === 'outgoing') {
250
+                    ctxSip.setCallSessionStatus('Connecting...');
251
+                }
252
+            });
253
+
254
+           newSess.on('accepted',function(e) {
255
+
256
+             // If there is another active call, hold it
257
+             if (ctxSip.callActiveID && ctxSip.callActiveID !== newSess.ctxid) {
258
+                 ctxSip.phoneHoldButtonPressed(ctxSip.callActiveID);
259
+             }
260
+
261
+             ctxSip.stopRingbackTone();
262
+             ctxSip.stopRingTone();
263
+             ctxSip.setCallSessionStatus('Answered');
264
+             ctxSip.logCall(newSess, 'answered');
265
+             ctxSip.callActiveID = newSess.ctxid;
266
+           });
267
+
268
+            newSess.on('hold', function(e) {
269
+                ctxSip.callActiveID = null;
270
+                ctxSip.logCall(newSess, 'holding');
271
+            });
272
+
273
+            newSess.on('unhold', function(e) {
274
+                ctxSip.logCall(newSess, 'resumed');
275
+                ctxSip.callActiveID = newSess.ctxid;
276
+            });
277
+
278
+            newSess.on('muted', function(e) {
279
+                ctxSip.Sessions[newSess.ctxid].isMuted = true;
280
+                ctxSip.setCallSessionStatus("Muted");
281
+            });
282
+
283
+            newSess.on('unmuted', function(e) {
284
+                ctxSip.Sessions[newSess.ctxid].isMuted = false;
285
+                ctxSip.setCallSessionStatus("Answered");
286
+            });
287
+
288
+            newSess.on('cancel', function(e) {
289
+                ctxSip.stopRingTone();
290
+                ctxSip.stopRingbackTone();
291
+                ctxSip.setCallSessionStatus("Canceled");
292
+                if (this.direction === 'outgoing') {
293
+                    ctxSip.callActiveID = null;
294
+                    newSess             = null;
295
+                    ctxSip.logCall(this, 'ended');
296
+                }
297
+            });
298
+
299
+            newSess.on('bye', function(e) {
300
+                ctxSip.stopRingTone();
301
+                ctxSip.stopRingbackTone();
302
+                ctxSip.setCallSessionStatus("");
303
+                ctxSip.logCall(newSess, 'ended');
304
+                ctxSip.callActiveID = null;
305
+                newSess             = null;
306
+            });
307
+
308
+            newSess.on('failed',function(e) {
309
+                ctxSip.stopRingTone();
310
+                ctxSip.stopRingbackTone();
311
+                ctxSip.setCallSessionStatus('Terminated');
312
+            });
313
+
314
+            newSess.on('rejected',function(e) {
315
+                ctxSip.stopRingTone();
316
+                ctxSip.stopRingbackTone();
317
+                ctxSip.setCallSessionStatus('Rejected');
318
+                ctxSip.callActiveID = null;
319
+                ctxSip.logCall(this, 'ended');
320
+                newSess             = null;
321
+            });
322
+
323
+            ctxSip.Sessions[newSess.ctxid] = newSess;
324
+
325
+        },
326
+
327
+        // getUser media request refused or device was not present
328
+        getUserMediaFailure : function(e) {
329
+            window.console.error('getUserMedia failed:', e);
330
+            ctxSip.setError(true, 'Media Error.', 'You must allow access to your microphone.  Check the address bar.', true);
331
+        },
332
+
333
+
334
+        getUserMediaSuccess : function(stream) {
335
+            ctxSip.Stream = stream;
336
+        },
337
+
338
+
339
+        /**
340
+         * sets the ui call status field
341
+         *
342
+         * @param {string} status
343
+         */
344
+        setCallSessionStatus : function(status) {
345
+            $('#txtCallStatus').html(status);
346
+        },
347
+
348
+        /**
349
+         * sets the ui connection status field
350
+         *
351
+         * @param {string} status
352
+         */
353
+        setStatus : function(status) {
354
+            $("#txtRegStatus").html('<i class="fa fa-signal"></i> '+status);
355
+        },
356
+
357
+        /**
358
+         * logs a call to localstorage
359
+         *
360
+         * @param  {object} session
361
+         * @param  {string} status Enum 'ringing', 'answered', 'ended', 'holding', 'resumed'
362
+         */
363
+        logCall : function(session, status) {
364
+
365
+            var log = {
366
+                    clid : session.displayName,
367
+                    uri  : session.remoteIdentity.uri.toString(),
368
+                    id   : session.ctxid,
369
+                    time : new Date().getTime()
370
+                },
371
+                calllog = JSON.parse(localStorage.getItem('sipCalls'));
372
+
373
+            if (!calllog) { calllog = {}; }
374
+
375
+            if (!calllog.hasOwnProperty(session.ctxid)) {
376
+                calllog[log.id] = {
377
+                    id    : log.id,
378
+                    clid  : log.clid,
379
+                    uri   : log.uri,
380
+                    start : log.time,
381
+                    flow  : session.direction
382
+                };
383
+            }
384
+
385
+            if (status === 'ended') {
386
+                calllog[log.id].stop = log.time;
387
+            }
388
+
389
+            if (status === 'ended' && calllog[log.id].status === 'ringing') {
390
+                calllog[log.id].status = 'missed';
391
+            } else {
392
+                calllog[log.id].status = status;
393
+            }
394
+
395
+            localStorage.setItem('sipCalls', JSON.stringify(calllog));
396
+            ctxSip.logShow();
397
+        },
398
+
399
+        /**
400
+         * adds a ui item to the call log
401
+         *
402
+         * @param  {object} item log item
403
+         */
404
+        logItem : function(item) {
405
+
406
+            var callActive = (item.status !== 'ended' && item.status !== 'missed'),
407
+                callLength = (item.status !== 'ended')? '<span id="'+item.id+'"></span>': moment.duration(item.stop - item.start).humanize(),
408
+                callClass  = '',
409
+                callIcon,
410
+                i;
411
+
412
+            switch (item.status) {
413
+                case 'ringing'  :
414
+                    callClass = 'list-group-item-success';
415
+                    callIcon  = 'fa-bell';
416
+                    break;
417
+
418
+                case 'missed'   :
419
+                    callClass = 'list-group-item-danger';
420
+                    if (item.flow === "incoming") { callIcon = 'fa-chevron-left'; }
421
+                    if (item.flow === "outgoing") { callIcon = 'fa-chevron-right'; }
422
+                    break;
423
+
424
+                case 'holding'  :
425
+                    callClass = 'list-group-item-warning';
426
+                    callIcon  = 'fa-pause';
427
+                    break;
428
+
429
+                case 'answered' :
430
+                case 'resumed'  :
431
+                    callClass = 'list-group-item-info';
432
+                    callIcon  = 'fa-phone-square';
433
+                    break;
434
+
435
+                case 'ended'  :
436
+                    if (item.flow === "incoming") { callIcon = 'fa-chevron-left'; }
437
+                    if (item.flow === "outgoing") { callIcon = 'fa-chevron-right'; }
438
+                    break;
439
+            }
440
+
441
+
442
+            i  = '<div class="list-group-item sip-logitem clearfix '+callClass+'" data-uri="'+item.uri+'" data-sessionid="'+item.id+'" title="Double click to call">';
443
+            i += '<div class="clearfix"><div class="pull-left">';
444
+            i += '<i class="fa fa-fw '+callIcon+' fa-fw"></i> <strong>'+ctxSip.formatPhone(item.uri)+'</strong><br><small>'+moment(item.start).format('YYYY/MM/DD HH:mm:ss')+'</small>';
445
+            i += '</div>';
446
+            i += '<div class="pull-right text-right"><em>'+item.clid+'</em><br>' + callLength+'</div></div>';
447
+
448
+            if (callActive) {
449
+                i += '<div class="btn-group btn-group-xs pull-right">';
450
+                if (item.status === 'ringing' && item.flow === 'incoming') {
451
+                    i += '<button class="btn btn-xs btn-success btnCall" title="Call"><i class="fa fa-phone"></i></button>';
452
+                } else {
453
+                    i += '<button class="btn btn-xs btn-primary btnHoldResume" title="Hold"><i class="fa fa-pause"></i></button>';
454
+                    i += '<button class="btn btn-xs btn-info btnTransfer" title="Transfer"><i class="fa fa-random"></i></button>';
455
+                    i += '<button class="btn btn-xs btn-warning btnMute" title="Mute"><i class="fa fa-fw fa-microphone"></i></button>';
456
+                }
457
+                i += '<button class="btn btn-xs btn-danger btnHangUp" title="Hangup"><i class="fa fa-stop"></i></button>';
458
+                i += '</div>';
459
+            }
460
+            i += '</div>';
461
+
462
+            $('#sip-logitems').append(i);
463
+
464
+
465
+            // Start call timer on answer
466
+            if (item.status === 'answered') {
467
+                var tEle = document.getElementById(item.id);
468
+                ctxSip.callTimers[item.id] = new Stopwatch(tEle);
469
+                ctxSip.callTimers[item.id].start();
470
+            }
471
+
472
+            if (callActive && item.status !== 'ringing') {
473
+                ctxSip.callTimers[item.id].start({startTime : item.start});
474
+            }
475
+
476
+            $('#sip-logitems').scrollTop(0);
477
+        },
478
+
479
+        /**
480
+         * updates the call log ui
481
+         */
482
+        logShow : function() {
483
+
484
+            var calllog = JSON.parse(localStorage.getItem('sipCalls')),
485
+            x = [];
486
+
487
+            if (calllog !== null) {
488
+
489
+                $('#sip-splash').addClass('hide');
490
+                $('#sip-log').removeClass('hide');
491
+
492
+                // empty existing logs
493
+                $('#sip-logitems').empty();
494
+
495
+                // JS doesn't guarantee property order so
496
+                // create an array with the start time as
497
+                // the key and sort by that.
498
+
499
+                // Add start time to array
500
+                $.each(calllog, function(k,v) {
501
+                    x.push(v);
502
+                });
503
+
504
+                // sort descending
505
+                x.sort(function(a, b) {
506
+                    return b.start - a.start;
507
+                });
508
+
509
+                $.each(x, function(k, v) {
510
+                    ctxSip.logItem(v);
511
+                });
512
+
513
+            } else {
514
+                $('#sip-splash').removeClass('hide');
515
+                $('#sip-log').addClass('hide');
516
+            }
517
+        },
518
+
519
+        /**
520
+         * removes log items from localstorage and updates the UI
521
+         */
522
+        logClear : function() {
523
+
524
+            localStorage.removeItem('sipCalls');
525
+            ctxSip.logShow();
526
+        },
527
+
528
+        sipCall : function(target) {
529
+
530
+            try {
531
+
532
+	        // Get the name of the SIP provider to send it to Asterisk, so that Asterisk knows what provider
533
+	        // to use for outbound calls, in case multiple phone numbers from different providers are used
534
+	        var callerNumber = $("#fromNumber").val();
535
+
536
+                if (callerNumber.indexOf(":") >= 0) {
537
+	            var shortProv = callerNumber.split(":");
538
+	            var sipProvider = shortProv[0].replace(" ", "");
539
+                    var callFromNumber = shortProv[1].replace(" ", "");
540
+                } else {
541
+	            var sipProvider = 'n/a';
542
+                    var callFromNumber = callerNumber;
543
+                }
544
+
545
+                var s = ctxSip.phone.invite(target, {
546
+                    media : {
547
+                        stream      : ctxSip.Stream,
548
+                        constraints : { audio : true, video : false },
549
+                        render      : { remote: document.getElementById('audioRemote') }
550
+                        // render: { remote: $('#audioRemote').get()[0] }
551
+                        // RTCConstraints : { "optional": [{ 'DtlsSrtpKeyAgreement': 'true'} ]}
552
+                    },
553
+                    extraHeaders    : [ 'X-SipProvider: '+ sipProvider , 'X-CallFromNumber: '+ callFromNumber ]
554
+                });
555
+                s.direction = 'outgoing';
556
+                ctxSip.newSession(s);
557
+
558
+            } catch(e) {
559
+                throw(e);
560
+            }
561
+        },
562
+
563
+        sipTransfer : function(sessionid) {
564
+
565
+                var s  = ctxSip.Sessions[sessionid],
566
+                target = window.prompt('Enter destination number', '');
567
+
568
+                if (target) {
569
+                    ctxSip.setCallSessionStatus('<i>Transfering the call...</i>');
570
+                }
571
+                s.refer(target);
572
+        },
573
+
574
+        sipHangUp : function(sessionid) {
575
+
576
+            var s = ctxSip.Sessions[sessionid];
577
+            // s.terminate();
578
+            if (!s) {
579
+                return;
580
+            } else if (s.startTime) {
581
+                s.bye();
582
+            } else if (s.reject) {
583
+                s.reject();
584
+            } else if (s.cancel) {
585
+                s.cancel();
586
+            }
587
+
588
+        },
589
+
590
+        sipSendDTMF : function(digit) {
591
+
592
+            // try { ctxSip.dtmfTone.play(); } catch(e) { }
593
+
594
+            var a = ctxSip.callActiveID;
595
+            if (a) {
596
+                var s = ctxSip.Sessions[a];
597
+                s.dtmf(digit);
598
+            }
599
+        },
600
+
601
+        phoneCallButtonPressed : function(sessionid) {
602
+
603
+                var s  = ctxSip.Sessions[sessionid];
604
+                var targetinit = $("#numDisplay").val();
605
+
606
+            if (!s) {
607
+
608
+		if (targetinit.trim() !== '') {
609
+		    if (/^[a-zA-Z0-9\+\*\#\@\:\.\-]+$/.test(targetinit) && targetinit.length < 201) {
610
+
611
+                        var target = targetinit;
612
+                        ctxSip.sipCall(target);
613
+
614
+                    } else {
615
+	                alert("The phone number or extension that you have tried to dial is not valid. You can only enter numbers, uppercase and lowercase letters, plus signs (+), asterisks (*), number signs (#), at signs (@), colons (:), periods (.) and hyphens (-) in the 'Recipient' field. Also, the total number of characters cannot exceed 200.");
616
+		    }
617
+                }
618
+
619
+            } else if (s.accept && !s.startTime) {
620
+
621
+                s.accept({
622
+                    media: {
623
+                            stream: ctxSip.Stream,
624
+                            constraints: { audio: true, video: false },
625
+                            render      : { remote: document.getElementById('audioRemote') }
626
+                            // render: { remote: $('#audioRemote').get()[0] }
627
+                            // RTCConstraints : { "optional": [{ 'DtlsSrtpKeyAgreement': 'true'} ]}
628
+                           }
629
+                });
630
+            }
631
+        },
632
+
633
+        phoneMuteButtonPressed : function (sessionid) {
634
+
635
+            var s = ctxSip.Sessions[sessionid];
636
+
637
+            if (!s.isMuted) {
638
+                s.mute();
639
+            } else {
640
+                s.unmute();
641
+            }
642
+        },
643
+
644
+        phoneHoldButtonPressed : function(sessionid) {
645
+
646
+            var s = ctxSip.Sessions[sessionid];
647
+
648
+            if (s.isOnHold().local === true) {
649
+                s.unhold();
650
+            } else {
651
+                s.hold();
652
+            }
653
+        },
654
+
655
+
656
+        setError : function(err, title, msg, closable) {
657
+
658
+            // Show modal if err = true
659
+            if (err === true) {
660
+                $("#mdlError p").html(msg);
661
+                $("#mdlError").modal('show');
662
+
663
+                if (closable) {
664
+                    var b = '<button type="button" class="close" data-dismiss="modal">&times;</button>';
665
+                    $("#mdlError .modal-header").find('button').remove();
666
+                    $("#mdlError .modal-header").prepend(b);
667
+                    $("#mdlError .modal-title").html(title);
668
+                    $("#mdlError").modal({ keyboard : true });
669
+                } else {
670
+                    $("#mdlError .modal-header").find('button').remove();
671
+                    $("#mdlError .modal-title").html(title);
672
+                    $("#mdlError").modal({ keyboard : false });
673
+                }
674
+                $('#numDisplay').prop('disabled', 'disabled');
675
+            } else {
676
+                $('#numDisplay').removeProp('disabled');
677
+                $("#mdlError").modal('hide');
678
+            }
679
+        },
680
+
681
+        /**
682
+         * Tests for a capable browser, return bool, and shows an
683
+         * error modal on fail.
684
+         */
685
+        hasWebRTC : function() {
686
+
687
+            if (navigator.webkitGetUserMedia) {
688
+                return true;
689
+            } else if (navigator.mozGetUserMedia) {
690
+                return true;
691
+            } else if (navigator.getUserMedia) {
692
+                return true;
693
+            } else {
694
+                ctxSip.setError(true, 'Unsupported Browser.', 'Your browser does not support the features required for this phone.');
695
+                window.console.error("WebRTC support not found");
696
+                return false;
697
+            }
698
+        }
699
+    };
700
+
701
+    userSIPPass = '';
702
+    window.opener.sipUserPasswd = '';
703
+
704
+    // Throw an error if the browser can't hack it.
705
+    if (!ctxSip.hasWebRTC()) {
706
+        return true;
707
+    }
708
+
709
+    ctxSip.phone = new SIP.UA(ctxSip.config);
710
+
711
+    ctxSip.phone.on('connected', function(e) {
712
+        ctxSip.setStatus("Connected");
713
+    });
714
+
715
+    ctxSip.phone.on('disconnected', function(e) {
716
+        ctxSip.setStatus("Disconnected");
717
+
718
+        // disable phone
719
+        ctxSip.setError(true, 'Websocket Disconnected.', 'An Error occurred connecting to the websocket.');
720
+
721
+        // remove existing sessions
722
+        $("#sessions > .session").each(function(i, session) {
723
+            ctxSip.removeSession(session, 500);
724
+        });
725
+    });
726
+
727
+    ctxSip.phone.on('registered', function(e) {
728
+
729
+        var closeEditorWarning = function() {
730
+            return 'If you close this window, you will not be able to make or receive calls from your browser.';
731
+        };
732
+
733
+        var closePhone = function() {
734
+            // stop the phone on unload
735
+            localStorage.removeItem('SipTripPhone');
736
+            ctxSip.phone.stop();
737
+        };
738
+
739
+        window.onbeforeunload = closeEditorWarning;
740
+        window.onunload       = closePhone;
741
+
742
+        // This key is set to prevent multiple windows.
743
+        localStorage.setItem('SipTripPhone', 'true');
744
+
745
+        $("#mldError").modal('hide');
746
+        ctxSip.setStatus("Ready");
747
+
748
+        // Get the userMedia and cache the stream
749
+        var audio = document.getElementById('audioRemote');
750
+        var mediaStream = new MediaStream();
751
+        let audioTrack = null;
752
+
753
+        navigator.mediaDevices.getUserMedia({ audio : true, video : false }, ctxSip.getUserMediaSuccess, ctxSip.getUserMediaFailure).then(function(mediaStream) {
754
+
755
+           let audioTracks = mediaStream.getAudioTracks();
756
+           audio.srcObject = mediaStream;
757
+
758
+           if (audioTracks.length) {
759
+               audioTrack = audioTracks[0];
760
+           }
761
+        }).then(function() {
762
+           new Promise(function(resolve) {
763
+               audio.onloadedmetadata = resolve;
764
+           })
765
+        })
766
+
767
+    });
768
+
769
+    ctxSip.phone.on('registrationFailed', function(e) {
770
+        ctxSip.setError(true, 'Registration Error.', 'An error occurred while registering your phone. Please check your settings.');
771
+        ctxSip.setStatus("Error: Registration Failed");
772
+    });
773
+
774
+    ctxSip.phone.on('unregistered', function(e) {
775
+        ctxSip.setError(true, 'Registration Error.', 'An error occurred while registering your phone. Please check your settings.');
776
+        ctxSip.setStatus("Error: Registration Failed");
777
+    });
778
+
779
+    ctxSip.phone.on('invite', function (incomingSession) {
780
+
781
+        var s = incomingSession;
782
+
783
+        s.direction = 'incoming';
784
+        ctxSip.newSession(s);
785
+    });
786
+
787
+    // Auto-focus number input on backspace.
788
+    $('#sipClient').keydown(function(event) {
789
+        if (event.which === 8) {
790
+            $('#numDisplay').focus();
791
+        }
792
+    });
793
+
794
+    $('#numDisplay').keypress(function(e) {
795
+        // Enter pressed? so Dial.
796
+        if (e.which === 13) {
797
+            ctxSip.phoneCallButtonPressed();
798
+        }
799
+    });
800
+
801
+    var clck = 0;
802
+
803
+    $('.digit').click(function(event) {
804
+
805
+     if (event.shiftKey) {
806
+
807
+         clck++;
808
+         event.preventDefault();
809
+         var num = $('#numDisplay').val();
810
+         var dig;
811
+         var diginit = $(this).data('digit').toString().split(',');
812
+         var elct = diginit.length;
813
+
814
+         dig = diginit[clck%elct];
815
+         var numsec = num.slice(0,-1);
816
+         $('#numDisplay').val(numsec+dig);
817
+         ctxSip.sipSendDTMF(dig);
818
+
819
+     } else {
820
+         event.preventDefault();
821
+         var num = $('#numDisplay').val();
822
+         var dig;
823
+         var diginit = $(this).data('digit').toString().split(',');
824
+
825
+         dig = diginit[0];
826
+         clck = 0;
827
+         $('#numDisplay').val(num+dig);
828
+         ctxSip.sipSendDTMF(dig);
829
+       }
830
+
831
+       return false;
832
+
833
+    });
834
+
835
+    $('#phoneUI .dropdown-menu').click(function(e) {
836
+        e.preventDefault();
837
+    });
838
+
839
+    $('#phoneUI').delegate('.btnCall', 'click', function(event) {
840
+        ctxSip.phoneCallButtonPressed();
841
+        // to close the drop-down
842
+        return true;
843
+    });
844
+
845
+    $('.sipLogClear').click(function(event) {
846
+        event.preventDefault();
847
+        ctxSip.logClear();
848
+    });
849
+
850
+    $('#sip-logitems').delegate('.sip-logitem .btnCall', 'click', function(event) {
851
+        var sessionid = $(this).closest('.sip-logitem').data('sessionid');
852
+        ctxSip.phoneCallButtonPressed(sessionid);
853
+        return false;
854
+    });
855
+
856
+    $('#sip-logitems').delegate('.sip-logitem .btnHoldResume', 'click', function(event) {
857
+        var sessionid = $(this).closest('.sip-logitem').data('sessionid');
858
+        ctxSip.phoneHoldButtonPressed(sessionid);
859
+        return false;
860
+    });
861
+
862
+    $('#sip-logitems').delegate('.sip-logitem .btnHangUp', 'click', function(event) {
863
+        var sessionid = $(this).closest('.sip-logitem').data('sessionid');
864
+        ctxSip.sipHangUp(sessionid);
865
+        return false;
866
+    });
867
+
868
+    $('#sip-logitems').delegate('.sip-logitem .btnTransfer', 'click', function(event) {
869
+        var sessionid = $(this).closest('.sip-logitem').data('sessionid');
870
+        ctxSip.sipTransfer(sessionid);
871
+        return false;
872
+    });
873
+
874
+    $('#sip-logitems').delegate('.sip-logitem .btnMute', 'click', function(event) {
875
+        var sessionid = $(this).closest('.sip-logitem').data('sessionid');
876
+        ctxSip.phoneMuteButtonPressed(sessionid);
877
+
878
+        var crtMuteBtn = $(this).closest('.sip-logitem').find('.btnMute');
879
+        if (crtMuteBtn.css("background-color") != "rgb(145, 103, 43)") {
880
+            crtMuteBtn.css("background-color", "rgb(145, 103, 43)");
881
+            crtMuteBtn.prop("title", "Unmute");
882
+        } else {
883
+            crtMuteBtn.css("background-color", "rgb(240, 173, 78)");
884
+            crtMuteBtn.prop("title", "Mute");
885
+        }
886
+
887
+        return false;
888
+    });
889
+
890
+    $('#sip-logitems').delegate('.sip-logitem', 'dblclick', function(event) {
891
+        event.preventDefault();
892
+
893
+        var uricalled = $(this).data('uri');
894
+        var uriproc = uricalled.split(":");
895
+        var uripronb = uriproc[1].split("@");
896
+        var uri = uripronb[0];
897
+        $('#numDisplay').val(uri);
898
+        ctxSip.phoneCallButtonPressed();
899
+    });
900
+
901
+    $('#sldVolume').on('change', function() {
902
+
903
+            var v  = $(this).val() / 100,
904
+            btn    = $('#btnVol'),
905
+            icon   = $('#btnVol').find('i'),
906
+            active = ctxSip.callActiveID;
907
+
908
+        // Set the object and media stream volumes
909
+        if (ctxSip.Sessions[active]) {
910
+            ctxSip.Sessions[active].player.volume = v;
911
+            ctxSip.callVolume                     = v;
912
+        }
913
+
914
+        // Set the others
915
+        $('audio').each(function() {
916
+            $(this).get()[0].volume = v;
917
+        });
918
+
919
+        if (v < 0.1) {
920
+            btn.removeClass(function (index, css) {
921
+                   return (css.match (/(^|\s)btn\S+/g) || []).join(' ');
922
+                })
923
+                .addClass('btn btn-sm btn-danger');
924
+            icon.removeClass().addClass('fa fa-fw fa-volume-off');
925
+        } else if (v < 0.8) {
926
+            btn.removeClass(function (index, css) {
927
+                   return (css.match (/(^|\s)btn\S+/g) || []).join(' ');
928
+               }).addClass('btn btn-sm btn-info');
929
+            icon.removeClass().addClass('fa fa-fw fa-volume-down');
930
+        } else {
931
+            btn.removeClass(function (index, css) {
932
+                   return (css.match (/(^|\s)btn\S+/g) || []).join(' ');
933
+               }).addClass('btn btn-sm btn-primary');
934
+            icon.removeClass().addClass('fa fa-fw fa-volume-up');
935
+        }
936
+        return false;
937
+    });
938
+
939
+    // Hide the spalsh after 3 secs.
940
+    setTimeout(function() {
941
+        ctxSip.logShow();
942
+    }, 3000);
943
+
944
+
945
+    /**
946
+     * Stopwatch object used for call timers
947
+     *
948
+     * @param {dom element} elem
949
+     * @param {[object]} options
950
+     */
951
+    var Stopwatch = function(elem, options) {
952
+
953
+        // Private functions
954
+        function createTimer() {
955
+            return document.createElement("span");
956
+        }
957
+
958
+        var timer = createTimer(),
959
+            offset,
960
+            clock,
961
+            interval;
962
+
963
+        // Default options
964
+        options           = options || {};
965
+        options.delay     = options.delay || 1000;
966
+        options.startTime = options.startTime || Date.now();
967
+
968
+        // Append elements
969
+        elem.appendChild(timer);
970
+
971
+        function start() {
972
+            if (!interval) {
973
+                offset   = options.startTime;
974
+                interval = setInterval(update, options.delay);
975
+            }
976
+        }
977
+
978
+        function stop() {
979
+            if (interval) {
980
+                clearInterval(interval);
981
+                interval = null;
982
+            }
983
+        }
984
+
985
+        function reset() {
986
+            clock = 0;
987
+            render();
988
+        }
989
+
990
+        function update() {
991
+            clock += delta();
992
+            render();
993
+        }
994
+
995
+        function render() {
996
+            timer.innerHTML = moment(clock).format('mm:ss');
997
+        }
998
+
999
+        function delta() {
1000
+            var now = Date.now(),
1001
+                d   = now - offset;
1002
+
1003
+            offset = now;
1004
+            return d;
1005
+        }
1006
+
1007
+        // Initialize
1008
+        reset();
1009
+
1010
+        // Public API
1011
+        this.start = start; //function() { start; }
1012
+        this.stop  = stop; //function() { stop; }
1013
+    };
1014
+
1015
+});
Browse code

removed appinfo/signature.json CHANGELOG.txt phone/index.html phone/scripts/app.js phone/css/ctxSip.css js/launchphone.js

DoubleBastionAdmin authored on 09/01/2024 22:33:50
Showing 1 changed files
1 1
deleted file mode 100644
... ...
@@ -1,1014 +0,0 @@
1
-/**
2
- * @copyright 2021 Double Bastion LLC <www.doublebastion.com>
3
- *
4
- * @author Double Bastion LLC
5
- *
6
- * @license GNU AGPL version 3 or any later version
7
- *
8
- * This program is free software; you can redistribute it and/or
9
- * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
10
- * License as published by the Free Software Foundation; either
11
- * version 3 of the License, or any later version.
12
- *
13
- * This program is distributed in the hope that it will be useful,
14
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
- * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
17
- *
18
- * You should have received a copy of the GNU Affero General Public
19
- * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
20
- *
21
- *
22
- *
23
- * This is a modified version of the original file "app.js".
24
- *
25
- * Below is the copyright notice of ctxSip phone (https://github.com/collecttix/ctxSip)
26
- * which also applies to the original "app.js" file, which was part of ctxSip phone:
27
- *
28
- *
29
- *  The MIT License (MIT)
30
- *
31
- *  Copyright (c) 2014 Collecttix
32
- *
33
- *  Permission is hereby granted, free of charge, to any person obtaining a copy
34
- *  of this software and associated documentation files (the "Software"), to deal
35
- *  in the Software without restriction, including without limitation the rights
36
- *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
37
- *  copies of the Software, and to permit persons to whom the Software is
38
- *  furnished to do so, subject to the following conditions:
39
- *
40
- *  The above copyright notice and this permission notice shall be included in
41
- *  all copies or substantial portions of the Software.
42
- *
43
- *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
44
- *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
45
- *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
46
- *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
47
- *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
48
- *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
49
- *  THE SOFTWARE.
50
- *
51
- */
52
-
53
-
54
-/* globals SIP, user, moment, Stopwatch */
55
-
56
-$(document).ready(function() {
57
-
58
-    var ctxSip;
59
-
60
-    // Show system notifications on incoming calls
61
-    function incomingCallNote() {
62
-       var noticeOptions = { body: "New incoming call !!!", icon: "images/sip_trip_phone_logo.svg" }
63
-       var inComingCallNotification = new Notification("SIP Trip Phone incoming call", noticeOptions);
64
-       inComingCallNotification.onclick = function (event) {
65
-         return;
66
-       }
67
-
68
-       if (document.hasFocus()) {
69
-           return;
70
-       } else { setTimeout(incomingCallNote, 8000); }
71
-    }
72
-
73
-    // Change page title on incoming calls
74
-    function changePageTitle() {
75
-        if ($(document).attr("title") == "SIP Trip Phone") { $(document).prop("title", "New call !!!"); } else { $(document).prop("title", "SIP Trip Phone"); }
76
-        if (document.hasFocus()) {
77
-            $(document).prop("title", "SIP Trip Phone");
78
-            return;
79
-        } else { setTimeout(changePageTitle, 460); }
80
-    }
81
-
82
-    var userSIPPass = window.opener.sipUserPasswd;
83
-
84
-    var user = JSON.parse(localStorage.getItem('SIPCreds'));
85
-
86
-    var traceornot = (user.Tracesipmsg == 1)? true : false;
87
-
88
-    // Add the 'available phone numbers' drop-down list
89
-    var stpVoiceNmbrs = user.Voicenumbers;
90
-    var stpVoiceNbOpt = '';
91
-    var stpVoiceNmbrsArr = [];
92
-
93
-    if (stpVoiceNmbrs != '') {
94
-
95
-	stpVoiceNmbrsArr = stpVoiceNmbrs.split(",");
96
-	var stpDefNmbrFdb = user.Defaultvoicenumber;
97
-
98
-	for (var v = 0; v < stpVoiceNmbrsArr.length; v++) {
99
-             stpVoiceNbOpt += '<option value="'+ stpVoiceNmbrsArr[v] +'" class="stpSlctFrmNmbrs">'+ stpVoiceNmbrsArr[v] +'</option>';
100
-	}
101
-    }
102
-    stpVoiceNbOpt += '<option value="'+ user.User +'" class="stpSlctFrmNmbrs">'+ user.User +'</option>';
103
-
104
-    $("#fromNumber").append(stpVoiceNbOpt);
105
-
106
-    if (stpDefNmbrFdb != '' && stpDefNmbrFdb != null) {
107
-        $("#fromNumber").val(stpDefNmbrFdb);
108
-    } else {
109
-        if (stpVoiceNmbrsArr.length > 0) {
110
-            $("#fromNumber").val(stpVoiceNmbrsArr[0]);
111
-        } else { $("#fromNumber").val(user.User); }
112
-    }
113
-
114
-    // Adjust height of text frame and call history list,
115
-    // and width of dial pad
116
-    $("#sip-splash").css("height", window.innerHeight - 135);
117
-    $("#sip-logitems").css("height", window.innerHeight - 175);
118
-
119
-    var calcWidth = parseInt(window.innerWidth - 30) + "px";
120
-    var calcHeight = parseInt(window.innerHeight - 136) + "px";
121
-    $("#sip-dialpad").css({ "width" : calcWidth, "height" : calcHeight });
122
-
123
-    var calcMargin =  parseInt(($("#sip-dialpad").height() - $("#dialpadWrap").height()) / 2)  + "px auto";
124
-    $("#dialpadWrap").css("margin", calcMargin);
125
-
126
-    $(window).resize(function() {
127
-      $("#sip-logitems").css("height", window.innerHeight - 175);
128
-      $("#sip-splash").css("height", window.innerHeight - 135);
129
-    });
130
-
131
-    if (user.Stun != '') {
132
-        var configComp = {
133
-               password        : userSIPPass,
134
-               displayName     : user.Display,
135
-               uri             : 'sip:'+ user.User +'@'+ user.Realm,
136
-               wsServers       : user.WSServer,
137
-               stunServers     : ["stun:"+ user.Stun],
138
-               traceSip        : traceornot,
139
-               log             : { level : 3 },
140
-               registerExpires : 9999999
141
-            };
142
-    } else {
143
-        var configComp = {
144
-               password        : userSIPPass,
145
-               displayName     : user.Display,
146
-               uri             : 'sip:'+ user.User +'@'+ user.Realm,
147
-               wsServers       : user.WSServer,
148
-               traceSip        : traceornot,
149
-               log             : { level : 3 },
150
-               registerExpires : 9999999
151
-            };
152
-    }
153
-
154
-    ctxSip = {
155
-
156
-        config : configComp,
157
-        ringtone     : document.getElementById('ringtone'),
158
-        ringbacktone : document.getElementById('ringbacktone'),
159
-        dtmfTone     : document.getElementById('dtmfTone'),
160
-
161
-        Sessions     : [],
162
-        callTimers   : {},
163
-        callActiveID : null,
164
-        callVolume   : 1,
165
-        Stream       : null,
166
-
167
-        /**
168
-         * Parses a SIP uri and returns a formatted phone number.
169
-         *
170
-         * @param  {string} phone number or uri to format
171
-         * @return {string}       formatted number
172
-         */
173
-        formatPhone : function(phone) {
174
-
175
-            var num;
176
-
177
-            if (phone.indexOf('@')) {
178
-                num =  phone.split('@')[0];
179
-            } else {
180
-                num = phone;
181
-            }
182
-
183
-            num = num.toString().replace(/[^0-9]/g, '');
184
-
185
-            if (num.length === 10) {
186
-                return '(' + num.substr(0, 3) + ') ' + num.substr(3, 3) + '-' + num.substr(6,4);
187
-            } else if (num.length === 11) {
188
-                return '(' + num.substr(1, 3) + ') ' + num.substr(4, 3) + '-' + num.substr(7,4);
189
-            } else {
190
-                return num;
191
-            }
192
-        },
193
-
194
-        // Sound methods
195
-        startRingTone : function() {
196
-            try { ctxSip.ringtone.play(); } catch (e) { }
197
-        },
198
-
199
-        stopRingTone : function() {
200
-            try { ctxSip.ringtone.pause(); } catch (e) { }
201
-        },
202
-
203
-        startRingbackTone : function() {
204
-            try { ctxSip.ringbacktone.play(); } catch (e) { }
205
-        },
206
-
207
-        stopRingbackTone : function() {
208
-            try { ctxSip.ringbacktone.pause(); } catch (e) { }
209
-        },
210
-
211
-        // Genereates a random string to ID a call
212
-        getUniqueID : function() {
213
-            return Math.random().toString(36).substr(2, 9);
214
-        },
215
-
216
-
217
-        newSession : function(newSess) {
218
-
219
-            newSess.displayName = newSess.remoteIdentity.displayName || newSess.remoteIdentity.uri.user;
220
-            newSess.ctxid       = ctxSip.getUniqueID();
221
-
222
-            var status;
223
-
224
-            if (newSess.direction === 'incoming') {
225
-                status = "Incoming: "+ newSess.displayName;
226
-                ctxSip.startRingTone();
227
-
228
-                incomingCallNote();
229
-                changePageTitle();
230
-
231
-            } else {
232
-                status = "Trying: "+ newSess.displayName;
233
-                ctxSip.startRingbackTone();
234
-            }
235
-
236
-            ctxSip.logCall(newSess, 'ringing');
237
-
238
-            ctxSip.setCallSessionStatus(status);
239
-
240
-            // EVENT CALLBACKS
241
-
242
-            newSess.on('progress',function(e) {
243
-                if (e.direction === 'outgoing') {
244
-                    ctxSip.setCallSessionStatus('Calling...');
245
-                }
246
-            });
247
-
248
-            newSess.on('connecting',function(e) {
249
-                if (e.direction === 'outgoing') {
250
-                    ctxSip.setCallSessionStatus('Connecting...');
251
-                }
252
-            });
253
-
254
-
255
-           newSess.on('accepted',function(e) {
256
-
257
-             // If there is another active call, hold it
258
-             if (ctxSip.callActiveID && ctxSip.callActiveID !== newSess.ctxid) {
259
-                 ctxSip.phoneHoldButtonPressed(ctxSip.callActiveID);
260
-             }
261
-
262
-             ctxSip.stopRingbackTone();
263
-             ctxSip.stopRingTone();
264
-             ctxSip.setCallSessionStatus('Answered');
265
-             ctxSip.logCall(newSess, 'answered');
266
-             ctxSip.callActiveID = newSess.ctxid;
267
-           });
268
-
269
-            newSess.on('hold', function(e) {
270
-                ctxSip.callActiveID = null;
271
-                ctxSip.logCall(newSess, 'holding');
272
-            });
273
-
274
-            newSess.on('unhold', function(e) {
275
-                ctxSip.logCall(newSess, 'resumed');
276
-                ctxSip.callActiveID = newSess.ctxid;
277
-            });
278
-
279
-            newSess.on('muted', function(e) {
280
-                ctxSip.Sessions[newSess.ctxid].isMuted = true;
281
-                ctxSip.setCallSessionStatus("Muted");
282
-            });
283
-
284
-            newSess.on('unmuted', function(e) {
285
-                ctxSip.Sessions[newSess.ctxid].isMuted = false;
286
-                ctxSip.setCallSessionStatus("Answered");
287
-            });
288
-
289
-            newSess.on('cancel', function(e) {
290
-                ctxSip.stopRingTone();
291
-                ctxSip.stopRingbackTone();
292
-                ctxSip.setCallSessionStatus("Canceled");
293
-                if (this.direction === 'outgoing') {
294
-                    ctxSip.callActiveID = null;
295
-                    newSess             = null;
296
-                    ctxSip.logCall(this, 'ended');
297
-                }
298
-            });
299
-
300
-            newSess.on('bye', function(e) {
301
-                ctxSip.stopRingTone();
302
-                ctxSip.stopRingbackTone();
303
-                ctxSip.setCallSessionStatus("");
304
-                ctxSip.logCall(newSess, 'ended');
305
-                ctxSip.callActiveID = null;
306
-                newSess             = null;
307
-            });
308
-
309
-            newSess.on('failed',function(e) {
310
-                ctxSip.stopRingTone();
311
-                ctxSip.stopRingbackTone();
312
-                ctxSip.setCallSessionStatus('Terminated');
313
-            });
314
-
315
-            newSess.on('rejected',function(e) {
316
-                ctxSip.stopRingTone();
317
-                ctxSip.stopRingbackTone();
318
-                ctxSip.setCallSessionStatus('Rejected');
319
-                ctxSip.callActiveID = null;
320
-                ctxSip.logCall(this, 'ended');
321
-                newSess             = null;
322
-            });
323
-
324
-            ctxSip.Sessions[newSess.ctxid] = newSess;
325
-
326
-        },
327
-
328
-        // getUser media request refused or device was not present
329
-        getUserMediaFailure : function(e) {
330
-            window.console.error('getUserMedia failed:', e);
331
-            ctxSip.setError(true, 'Media Error.', 'You must allow access to your microphone.  Check the address bar.', true);
332
-        },
333
-
334
-
335
-        getUserMediaSuccess : function(stream) {
336
-            ctxSip.Stream = stream;
337
-        },
338
-
339
-
340
-        /**
341
-         * sets the ui call status field
342
-         *
343
-         * @param {string} status
344
-         */
345
-        setCallSessionStatus : function(status) {
346
-            $('#txtCallStatus').html(status);
347
-        },
348
-
349
-        /**
350
-         * sets the ui connection status field
351
-         *
352
-         * @param {string} status
353
-         */
354
-        setStatus : function(status) {
355
-            $("#txtRegStatus").html('<i class="fa fa-signal"></i> '+status);
356
-        },
357
-
358
-        /**
359
-         * logs a call to localstorage
360
-         *
361
-         * @param  {object} session
362
-         * @param  {string} status Enum 'ringing', 'answered', 'ended', 'holding', 'resumed'
363
-         */
364
-        logCall : function(session, status) {
365
-
366
-            var log = {
367
-                    clid : session.displayName,
368
-                    uri  : session.remoteIdentity.uri.toString(),
369
-                    id   : session.ctxid,
370
-                    time : new Date().getTime()
371
-                },
372
-                calllog = JSON.parse(localStorage.getItem('sipCalls'));
373
-
374
-            if (!calllog) { calllog = {}; }
375
-
376
-            if (!calllog.hasOwnProperty(session.ctxid)) {
377
-                calllog[log.id] = {
378
-                    id    : log.id,
379
-                    clid  : log.clid,
380
-                    uri   : log.uri,
381
-                    start : log.time,
382
-                    flow  : session.direction
383
-                };
384
-            }
385
-
386
-            if (status === 'ended') {
387
-                calllog[log.id].stop = log.time;
388
-            }
389
-
390
-            if (status === 'ended' && calllog[log.id].status === 'ringing') {
391
-                calllog[log.id].status = 'missed';
392
-            } else {
393
-                calllog[log.id].status = status;
394
-            }
395
-
396
-            localStorage.setItem('sipCalls', JSON.stringify(calllog));
397
-            ctxSip.logShow();
398
-        },
399
-
400
-        /**
401
-         * adds a ui item to the call log
402
-         *
403
-         * @param  {object} item log item
404
-         */
405
-        logItem : function(item) {
406
-
407
-            var callActive = (item.status !== 'ended' && item.status !== 'missed'),
408
-                callLength = (item.status !== 'ended')? '<span id="'+item.id+'"></span>': moment.duration(item.stop - item.start).humanize(),
409
-                callClass  = '',
410
-                callIcon,
411
-                i;
412
-
413
-            switch (item.status) {
414
-                case 'ringing'  :
415
-                    callClass = 'list-group-item-success';
416
-                    callIcon  = 'fa-bell';
417
-                    break;
418
-
419
-                case 'missed'   :
420
-                    callClass = 'list-group-item-danger';
421
-                    if (item.flow === "incoming") { callIcon = 'fa-chevron-left'; }
422
-                    if (item.flow === "outgoing") { callIcon = 'fa-chevron-right'; }
423
-                    break;
424
-
425
-                case 'holding'  :
426
-                    callClass = 'list-group-item-warning';
427
-                    callIcon  = 'fa-pause';
428
-                    break;
429
-
430
-                case 'answered' :
431
-                case 'resumed'  :
432
-                    callClass = 'list-group-item-info';
433
-                    callIcon  = 'fa-phone-square';
434
-                    break;
435
-
436
-                case 'ended'  :
437
-                    if (item.flow === "incoming") { callIcon = 'fa-chevron-left'; }
438
-                    if (item.flow === "outgoing") { callIcon = 'fa-chevron-right'; }
439
-                    break;
440
-            }
441
-
442
-
443
-            i  = '<div class="list-group-item sip-logitem clearfix '+callClass+'" data-uri="'+item.uri+'" data-sessionid="'+item.id+'" title="Double Click to Call">';
444
-            i += '<div class="clearfix"><div class="pull-left">';
445
-            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>';
446
-            i += '</div>';
447
-            i += '<div class="pull-right text-right"><em>'+item.clid+'</em><br>' + callLength+'</div></div>';
448
-
449
-            if (callActive) {
450
-                i += '<div class="btn-group btn-group-xs pull-right">';
451
-                if (item.status === 'ringing' && item.flow === 'incoming') {
452
-                    i += '<button class="btn btn-xs btn-success btnCall" title="Call"><i class="fa fa-phone"></i></button>';
453
-                } else {
454
-                    i += '<button class="btn btn-xs btn-primary btnHoldResume" title="Hold"><i class="fa fa-pause"></i></button>';
455
-                    i += '<button class="btn btn-xs btn-info btnTransfer" title="Transfer"><i class="fa fa-random"></i></button>';
456
-                    i += '<button class="btn btn-xs btn-warning btnMute" title="Mute"><i class="fa fa-fw fa-microphone"></i></button>';
457
-                }
458
-                i += '<button class="btn btn-xs btn-danger btnHangUp" title="Hangup"><i class="fa fa-stop"></i></button>';
459
-                i += '</div>';
460
-            }
461
-            i += '</div>';
462
-
463
-            $('#sip-logitems').append(i);
464
-
465
-
466
-            // Start call timer on answer
467
-            if (item.status === 'answered') {
468
-                var tEle = document.getElementById(item.id);
469
-                ctxSip.callTimers[item.id] = new Stopwatch(tEle);
470
-                ctxSip.callTimers[item.id].start();
471
-            }
472
-
473
-            if (callActive && item.status !== 'ringing') {
474
-                ctxSip.callTimers[item.id].start({startTime : item.start});
475
-            }
476
-
477
-            $('#sip-logitems').scrollTop(0);
478
-        },
479
-
480
-        /**
481
-         * updates the call log ui
482
-         */
483
-        logShow : function() {
484
-
485
-            var calllog = JSON.parse(localStorage.getItem('sipCalls')),
486
-            x = [];
487
-
488
-            if (calllog !== null) {
489
-
490
-                $('#sip-splash').addClass('hide');
491
-                $('#sip-log').removeClass('hide');
492
-
493
-                // empty existing logs
494
-                $('#sip-logitems').empty();
495
-
496
-                // JS doesn't guarantee property order so
497
-                // create an array with the start time as
498
-                // the key and sort by that.
499
-
500
-                // Add start time to array
501
-                $.each(calllog, function(k,v) {
502
-                    x.push(v);
503
-                });
504
-
505
-                // sort descending
506
-                x.sort(function(a, b) {
507
-                    return b.start - a.start;
508
-                });
509
-
510
-                $.each(x, function(k, v) {
511
-                    ctxSip.logItem(v);
512
-                });
513
-
514
-            } else {
515
-                $('#sip-splash').removeClass('hide');
516
-                $('#sip-log').addClass('hide');
517
-            }
518
-        },
519
-
520
-        /**
521
-         * removes log items from localstorage and updates the UI
522
-         */
523
-        logClear : function() {
524
-
525
-            localStorage.removeItem('sipCalls');
526
-            ctxSip.logShow();
527
-        },
528
-
529
-        sipCall : function(target) {
530
-
531
-            try {
532
-
533
-	        // Get the name of the SIP provider to send it to Asterisk, so that Asterisk knows what provider
534
-	        // to use for outbound calls, in case multiple phone numbers from different providers are used
535
-	        var callerNumber = $("#fromNumber").val();
536
-
537
-                if (callerNumber.indexOf(":") >= 0) {
538
-	            var shortProv = callerNumber.split(":");
539
-	            var sipProvider = shortProv[0].replace(" ", "");
540
-                    var callFromNumber = shortProv[1].replace(" ", "");
541
-                } else {
542
-	            var sipProvider = 'n/a';
543
-                    var callFromNumber = callerNumber;
544
-                }
545
-
546
-                var s = ctxSip.phone.invite(target, {
547
-                    media : {
548
-                        stream      : ctxSip.Stream,
549
-                        constraints : { audio : true, video : false },
550
-                        render      : { remote: document.getElementById('audioRemote') }
551
-                        // render: { remote: $('#audioRemote').get()[0] }
552
-                        // RTCConstraints : { "optional": [{ 'DtlsSrtpKeyAgreement': 'true'} ]}
553
-                    },
554
-                    extraHeaders    : [ 'X-SipProvider: '+ sipProvider , 'X-CallFromNumber: '+ callFromNumber ]
555
-                });
556
-                s.direction = 'outgoing';
557
-                ctxSip.newSession(s);
558
-
559
-            } catch(e) {
560
-                throw(e);
561
-            }
562
-        },
563
-
564
-        sipTransfer : function(sessionid) {
565
-
566
-                var s  = ctxSip.Sessions[sessionid],
567
-                target = window.prompt('Enter destination number', '');
568
-
569
-                if (target) {
570
-                    ctxSip.setCallSessionStatus('<i>Transfering the call...</i>');
571
-                }
572
-                s.refer(target);
573
-        },
574
-
575
-        sipHangUp : function(sessionid) {
576
-
577
-            var s = ctxSip.Sessions[sessionid];
578
-            // s.terminate();
579
-            if (!s) {
580
-                return;
581
-            } else if (s.startTime) {
582
-                s.bye();
583
-            } else if (s.reject) {
584
-                s.reject();
585
-            } else if (s.cancel) {
586
-                s.cancel();
587
-            }
588
-
589
-        },
590
-
591
-        sipSendDTMF : function(digit) {
592
-
593
-            try { ctxSip.dtmfTone.play(); } catch(e) { }
594
-
595
-            var a = ctxSip.callActiveID;
596
-            if (a) {
597
-                var s = ctxSip.Sessions[a];
598
-                s.dtmf(digit);
599
-            }
600
-        },
601
-
602
-        phoneCallButtonPressed : function(sessionid) {
603
-
604
-                var s  = ctxSip.Sessions[sessionid];
605
-//                target = $("#numDisplay").val();
606
-                var targetinit = $("#numDisplay").val();
607
-
608
-            if (!s) {
609
-
610
-		if (targetinit.trim() !== '') {
611
-		    if (/^[a-zA-Z0-9\+\*\#]+$/.test(targetinit) && targetinit.length < 201) {
612
-
613
-                        var target = targetinit;
614
-//                        $("#numDisplay").val("");
615
-                        ctxSip.sipCall(target);
616
-
617
-                    } else {
618
-	                alert("The phone number or extension that you have tried to dial is not valid. You can only enter numbers, uppercase and lowercase letters, plus signs (+), asterisks (*) and number signs (#) in the 'Recipient' field. Also, the total number of characters cannot exceed 200.");
619
-		    }
620
-                }
621
-
622
-            } else if (s.accept && !s.startTime) {
623
-
624
-                s.accept({
625
-                    media: {
626
-                            stream: ctxSip.Stream,
627
-                            constraints: { audio: true, video: false },
628
-                            render      : { remote: document.getElementById('audioRemote') }
629
-                            // render: { remote: $('#audioRemote').get()[0] }
630
-                            // RTCConstraints : { "optional": [{ 'DtlsSrtpKeyAgreement': 'true'} ]}
631
-                           }
632
-                });
633
-            }
634
-        },
635
-
636
-        phoneMuteButtonPressed : function (sessionid) {
637
-
638
-            var s = ctxSip.Sessions[sessionid];
639
-
640
-            if (!s.isMuted) {
641
-                s.mute();
642
-            } else {
643
-                s.unmute();
644
-            }
645
-        },
646
-
647
-        phoneHoldButtonPressed : function(sessionid) {
648
-
649
-            var s = ctxSip.Sessions[sessionid];
650
-
651
-            if (s.isOnHold().local === true) {
652
-                s.unhold();
653
-            } else {
654
-                s.hold();
655
-            }
656
-        },
657
-
658
-
659
-        setError : function(err, title, msg, closable) {
660
-
661
-            // Show modal if err = true
662
-            if (err === true) {
663
-                $("#mdlError p").html(msg);
664
-                $("#mdlError").modal('show');
665
-
666
-                if (closable) {
667
-                    var b = '<button type="button" class="close" data-dismiss="modal">&times;</button>';
668
-                    $("#mdlError .modal-header").find('button').remove();
669
-                    $("#mdlError .modal-header").prepend(b);
670
-                    $("#mdlError .modal-title").html(title);
671
-                    $("#mdlError").modal({ keyboard : true });
672
-                } else {
673
-                    $("#mdlError .modal-header").find('button').remove();
674
-                    $("#mdlError .modal-title").html(title);
675
-                    $("#mdlError").modal({ keyboard : false });
676
-                }
677
-                $('#numDisplay').prop('disabled', 'disabled');
678
-            } else {
679
-                $('#numDisplay').removeProp('disabled');
680
-                $("#mdlError").modal('hide');
681
-            }
682
-        },
683
-
684
-        /**
685
-         * Tests for a capable browser, return bool, and shows an
686
-         * error modal on fail.
687
-         */
688
-        hasWebRTC : function() {
689
-
690
-            if (navigator.webkitGetUserMedia) {
691
-                return true;
692
-            } else if (navigator.mozGetUserMedia) {
693
-                return true;
694
-            } else if (navigator.getUserMedia) {
695
-                return true;
696
-            } else {
697
-                ctxSip.setError(true, 'Unsupported Browser.', 'Your browser does not support the features required for this phone.');
698
-                window.console.error("WebRTC support not found");
699
-                return false;
700
-            }
701
-        }
702
-    };
703
-
704
-    userSIPPass = '';
705
-    window.opener.sipUserPasswd = '';
706
-
707
-    // Throw an error if the browser can't hack it.
708
-    if (!ctxSip.hasWebRTC()) {
709
-        return true;
710
-    }
711
-
712
-    ctxSip.phone = new SIP.UA(ctxSip.config);
713
-
714
-    ctxSip.phone.on('connected', function(e) {
715
-        ctxSip.setStatus("Connected");
716
-    });
717
-
718
-    ctxSip.phone.on('disconnected', function(e) {
719
-        ctxSip.setStatus("Disconnected");
720
-
721
-        // disable phone
722
-        ctxSip.setError(true, 'Websocket Disconnected.', 'An Error occurred connecting to the websocket.');
723
-
724
-        // remove existing sessions
725
-        $("#sessions > .session").each(function(i, session) {
726
-            ctxSip.removeSession(session, 500);
727
-        });
728
-    });
729
-
730
-    ctxSip.phone.on('registered', function(e) {
731
-
732
-        var closeEditorWarning = function() {
733
-            return 'If you close this window, you will not be able to make or receive calls from your browser.';
734
-        };
735
-
736
-        var closePhone = function() {
737
-            // stop the phone on unload
738
-            localStorage.removeItem('SipTripPhone');
739
-            ctxSip.phone.stop();
740
-        };
741
-
742
-        window.onbeforeunload = closeEditorWarning;
743
-        window.onunload       = closePhone;
744
-
745
-        // This key is set to prevent multiple windows.
746
-        localStorage.setItem('SipTripPhone', 'true');
747
-
748
-        $("#mldError").modal('hide');
749
-        ctxSip.setStatus("Ready");
750
-
751
-        // Get the userMedia and cache the stream
752
-        var audio = document.getElementById('audioRemote');
753
-        var mediaStream = new MediaStream();
754
-        let audioTrack = null;
755
-
756
-        navigator.mediaDevices.getUserMedia({ audio : true, video : false }, ctxSip.getUserMediaSuccess, ctxSip.getUserMediaFailure).then(function(mediaStream) {
757
-
758
-           let audioTracks = mediaStream.getAudioTracks();
759
-           audio.srcObject = mediaStream;
760
-
761
-           if (audioTracks.length) {
762
-               audioTrack = audioTracks[0];
763
-           }
764
-        }).then(function() {
765
-           new Promise(function(resolve) {
766
-               audio.onloadedmetadata = resolve;
767
-           })
768
-        })
769
-
770
-    });
771
-
772
-    ctxSip.phone.on('registrationFailed', function(e) {
773
-        ctxSip.setError(true, 'Registration Error.', 'An error occurred while registering your phone. Please check your settings.');
774
-        ctxSip.setStatus("Error: Registration Failed");
775
-    });
776
-
777
-    ctxSip.phone.on('unregistered', function(e) {
778
-        ctxSip.setError(true, 'Registration Error.', 'An error occurred while registering your phone. Please check your settings.');
779
-        ctxSip.setStatus("Error: Registration Failed");
780
-    });
781
-
782
-    ctxSip.phone.on('invite', function (incomingSession) {
783
-
784
-        var s = incomingSession;
785
-
786
-        s.direction = 'incoming';
787
-        ctxSip.newSession(s);
788
-    });
789
-
790
-    // Auto-focus number input on backspace.
791
-    $('#sipClient').keydown(function(event) {
792
-        if (event.which === 8) {
793
-            $('#numDisplay').focus();
794
-        }
795
-    });
796
-
797
-    $('#numDisplay').keypress(function(e) {
798
-        // Enter pressed? so Dial.
799
-        if (e.which === 13) {
800
-            ctxSip.phoneCallButtonPressed();
801
-        }
802
-    });
803
-
804
-    var clck = 0;
805
-
806
-    $('.digit').click(function(event) {
807
-
808
-     if (event.shiftKey) {
809
-
810
-         clck++;
811
-         event.preventDefault();
812
-         var num = $('#numDisplay').val();
813
-         var dig;
814
-         var diginit = $(this).data('digit').toString().split(',');
815
-         var elct = diginit.length;
816
-
817
-         dig = diginit[clck%elct];
818
-         var numsec = num.slice(0,-1);
819
-         $('#numDisplay').val(numsec+dig);
820
-         ctxSip.sipSendDTMF(dig);
821
-
822
-     } else {
823
-         event.preventDefault();
824
-         var num = $('#numDisplay').val();
825
-         var dig;
826
-         var diginit = $(this).data('digit').toString().split(',');
827
-
828
-         dig = diginit[0];
829
-         clck = 0;
830
-         $('#numDisplay').val(num+dig);
831
-         ctxSip.sipSendDTMF(dig);
832
-       }
833
-
834
-       return false;
835
-
836
-    });
837
-
838
-    $('#phoneUI .dropdown-menu').click(function(e) {
839
-        e.preventDefault();
840
-    });
841
-
842
-    $('#phoneUI').delegate('.btnCall', 'click', function(event) {
843
-        ctxSip.phoneCallButtonPressed();
844
-        // to close the dropdown
845
-        return true;
846
-    });
847
-
848
-    $('.sipLogClear').click(function(event) {
849
-        event.preventDefault();
850
-        ctxSip.logClear();
851
-    });
852
-
853
-    $('#sip-logitems').delegate('.sip-logitem .btnCall', 'click', function(event) {
854
-        var sessionid = $(this).closest('.sip-logitem').data('sessionid');
855
-        ctxSip.phoneCallButtonPressed(sessionid);
856
-        return false;
857
-    });
858
-
859
-    $('#sip-logitems').delegate('.sip-logitem .btnHoldResume', 'click', function(event) {
860
-        var sessionid = $(this).closest('.sip-logitem').data('sessionid');
861
-        ctxSip.phoneHoldButtonPressed(sessionid);
862
-        return false;
863
-    });
864
-
865
-    $('#sip-logitems').delegate('.sip-logitem .btnHangUp', 'click', function(event) {
866
-        var sessionid = $(this).closest('.sip-logitem').data('sessionid');
867
-        ctxSip.sipHangUp(sessionid);
868
-        return false;
869
-    });
870
-
871
-    $('#sip-logitems').delegate('.sip-logitem .btnTransfer', 'click', function(event) {
872
-        var sessionid = $(this).closest('.sip-logitem').data('sessionid');
873
-        ctxSip.sipTransfer(sessionid);
874
-        return false;
875
-    });
876
-
877
-    $('#sip-logitems').delegate('.sip-logitem .btnMute', 'click', function(event) {
878
-        var sessionid = $(this).closest('.sip-logitem').data('sessionid');
879
-        ctxSip.phoneMuteButtonPressed(sessionid);
880
-
881
-        var crtMuteBtn = $(this).closest('.sip-logitem').find('.btnMute');
882
-        if (crtMuteBtn.css("background-color") != "rgb(145, 103, 43)") {
883
-            crtMuteBtn.css("background-color", "rgb(145, 103, 43)");
884
-            crtMuteBtn.prop("title", "Unmute");
885
-        } else {
886
-            crtMuteBtn.css("background-color", "rgb(240, 173, 78)");
887
-            crtMuteBtn.prop("title", "Mute");
888
-        }
889
-        return false;
890
-    });
891
-
892
-    $('#sip-logitems').delegate('.sip-logitem', 'dblclick', function(event) {
893
-        event.preventDefault();
894
-
895
-        var uri = $(this).data('uri');
896
-        $('#numDisplay').val(uri);
897
-        ctxSip.phoneCallButtonPressed();
898
-    });
899
-
900
-    $('#sldVolume').on('change', function() {
901
-
902
-            var v  = $(this).val() / 100,
903
-            btn    = $('#btnVol'),
904
-            icon   = $('#btnVol').find('i'),
905
-            active = ctxSip.callActiveID;
906
-
907
-        // Set the object and media stream volumes
908
-        if (ctxSip.Sessions[active]) {
909
-            ctxSip.Sessions[active].player.volume = v;
910
-            ctxSip.callVolume                     = v;
911
-        }
912
-
913
-        // Set the others
914
-        $('audio').each(function() {
915
-            $(this).get()[0].volume = v;
916
-        });
917
-
918
-        if (v < 0.1) {
919
-            btn.removeClass(function (index, css) {
920
-                   return (css.match (/(^|\s)btn\S+/g) || []).join(' ');
921
-                })
922
-                .addClass('btn btn-sm btn-danger');
923
-            icon.removeClass().addClass('fa fa-fw fa-volume-off');
924
-        } else if (v < 0.8) {
925
-            btn.removeClass(function (index, css) {
926
-                   return (css.match (/(^|\s)btn\S+/g) || []).join(' ');
927
-               }).addClass('btn btn-sm btn-info');
928
-            icon.removeClass().addClass('fa fa-fw fa-volume-down');
929
-        } else {
930
-            btn.removeClass(function (index, css) {
931
-                   return (css.match (/(^|\s)btn\S+/g) || []).join(' ');
932
-               }).addClass('btn btn-sm btn-primary');
933
-            icon.removeClass().addClass('fa fa-fw fa-volume-up');
934
-        }
935
-        return false;
936
-    });
937
-
938
-    // Hide the spalsh after 3 secs.
939
-    setTimeout(function() {
940
-        ctxSip.logShow();
941
-    }, 3000);
942
-
943
-
944
-    /**
945
-     * Stopwatch object used for call timers
946
-     *
947
-     * @param {dom element} elem
948
-     * @param {[object]} options
949
-     */
950
-    var Stopwatch = function(elem, options) {
951
-
952
-        // private functions
953
-        function createTimer() {
954
-            return document.createElement("span");
955
-        }
956
-
957
-        var timer = createTimer(),
958
-            offset,
959
-            clock,
960
-            interval;
961
-
962
-        // default options
963
-        options           = options || {};
964
-        options.delay     = options.delay || 1000;
965
-        options.startTime = options.startTime || Date.now();
966
-
967
-        // append elements
968
-        elem.appendChild(timer);
969
-
970
-        function start() {
971
-            if (!interval) {
972
-                offset   = options.startTime;
973
-                interval = setInterval(update, options.delay);
974
-            }
975
-        }
976
-
977
-        function stop() {
978
-            if (interval) {
979
-                clearInterval(interval);
980
-                interval = null;
981
-            }
982
-        }
983
-
984
-        function reset() {
985
-            clock = 0;
986
-            render();
987
-        }
988
-
989
-        function update() {
990
-            clock += delta();
991
-            render();
992
-        }
993
-
994
-        function render() {
995
-            timer.innerHTML = moment(clock).format('mm:ss');
996
-        }
997
-
998
-        function delta() {
999
-            var now = Date.now(),
1000
-                d   = now - offset;
1001
-
1002
-            offset = now;
1003
-            return d;
1004
-        }
1005
-
1006
-        // initialize
1007
-        reset();
1008
-
1009
-        // public API
1010
-        this.start = start; //function() { start; }
1011
-        this.stop  = stop; //function() { stop; }
1012
-    };
1013
-
1014
-});
Browse code

added changes to implement the From number drop-down list, the available numbers and default number fields and the debug logging checkbox, etc.

DoubleBastionAdmin authored on 08/01/2024 19:33:20
Showing 1 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,1014 @@
1
+/**
2
+ * @copyright 2021 Double Bastion LLC <www.doublebastion.com>
3
+ *
4
+ * @author Double Bastion LLC
5
+ *
6
+ * @license GNU AGPL version 3 or any later version
7
+ *
8
+ * This program is free software; you can redistribute it and/or
9
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
10
+ * License as published by the Free Software Foundation; either
11
+ * version 3 of the License, or any later version.
12
+ *
13
+ * This program is distributed in the hope that it will be useful,
14
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
17
+ *
18
+ * You should have received a copy of the GNU Affero General Public
19
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
20
+ *
21
+ *
22
+ *
23
+ * This is a modified version of the original file "app.js".
24
+ *
25
+ * Below is the copyright notice of ctxSip phone (https://github.com/collecttix/ctxSip)
26
+ * which also applies to the original "app.js" file, which was part of ctxSip phone:
27
+ *
28
+ *
29
+ *  The MIT License (MIT)
30
+ *
31
+ *  Copyright (c) 2014 Collecttix
32
+ *
33
+ *  Permission is hereby granted, free of charge, to any person obtaining a copy
34
+ *  of this software and associated documentation files (the "Software"), to deal
35
+ *  in the Software without restriction, including without limitation the rights
36
+ *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
37
+ *  copies of the Software, and to permit persons to whom the Software is
38
+ *  furnished to do so, subject to the following conditions:
39
+ *
40
+ *  The above copyright notice and this permission notice shall be included in
41
+ *  all copies or substantial portions of the Software.
42
+ *
43
+ *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
44
+ *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
45
+ *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
46
+ *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
47
+ *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
48
+ *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
49
+ *  THE SOFTWARE.
50
+ *
51
+ */
52
+
53
+
54
+/* globals SIP, user, moment, Stopwatch */
55
+
56
+$(document).ready(function() {
57
+
58
+    var ctxSip;
59
+
60
+    // Show system notifications on incoming calls
61
+    function incomingCallNote() {
62
+       var noticeOptions = { body: "New incoming call !!!", icon: "images/sip_trip_phone_logo.svg" }
63
+       var inComingCallNotification = new Notification("SIP Trip Phone incoming call", noticeOptions);
64
+       inComingCallNotification.onclick = function (event) {
65
+         return;
66
+       }
67
+
68
+       if (document.hasFocus()) {
69
+           return;
70
+       } else { setTimeout(incomingCallNote, 8000); }
71
+    }
72
+
73
+    // Change page title on incoming calls
74
+    function changePageTitle() {
75
+        if ($(document).attr("title") == "SIP Trip Phone") { $(document).prop("title", "New call !!!"); } else { $(document).prop("title", "SIP Trip Phone"); }
76
+        if (document.hasFocus()) {
77
+            $(document).prop("title", "SIP Trip Phone");
78
+            return;
79
+        } else { setTimeout(changePageTitle, 460); }
80
+    }
81
+
82
+    var userSIPPass = window.opener.sipUserPasswd;
83
+
84
+    var user = JSON.parse(localStorage.getItem('SIPCreds'));
85
+
86
+    var traceornot = (user.Tracesipmsg == 1)? true : false;
87
+
88
+    // Add the 'available phone numbers' drop-down list
89
+    var stpVoiceNmbrs = user.Voicenumbers;
90
+    var stpVoiceNbOpt = '';
91
+    var stpVoiceNmbrsArr = [];
92
+
93
+    if (stpVoiceNmbrs != '') {
94
+
95
+	stpVoiceNmbrsArr = stpVoiceNmbrs.split(",");
96
+	var stpDefNmbrFdb = user.Defaultvoicenumber;
97
+
98
+	for (var v = 0; v < stpVoiceNmbrsArr.length; v++) {
99
+             stpVoiceNbOpt += '<option value="'+ stpVoiceNmbrsArr[v] +'" class="stpSlctFrmNmbrs">'+ stpVoiceNmbrsArr[v] +'</option>';
100
+	}
101
+    }
102
+    stpVoiceNbOpt += '<option value="'+ user.User +'" class="stpSlctFrmNmbrs">'+ user.User +'</option>';
103
+
104
+    $("#fromNumber").append(stpVoiceNbOpt);
105
+
106
+    if (stpDefNmbrFdb != '' && stpDefNmbrFdb != null) {
107
+        $("#fromNumber").val(stpDefNmbrFdb);
108
+    } else {
109
+        if (stpVoiceNmbrsArr.length > 0) {
110
+            $("#fromNumber").val(stpVoiceNmbrsArr[0]);
111
+        } else { $("#fromNumber").val(user.User); }
112
+    }
113
+
114
+    // Adjust height of text frame and call history list,
115
+    // and width of dial pad
116
+    $("#sip-splash").css("height", window.innerHeight - 135);
117
+    $("#sip-logitems").css("height", window.innerHeight - 175);
118
+
119
+    var calcWidth = parseInt(window.innerWidth - 30) + "px";
120
+    var calcHeight = parseInt(window.innerHeight - 136) + "px";
121
+    $("#sip-dialpad").css({ "width" : calcWidth, "height" : calcHeight });
122
+
123
+    var calcMargin =  parseInt(($("#sip-dialpad").height() - $("#dialpadWrap").height()) / 2)  + "px auto";
124
+    $("#dialpadWrap").css("margin", calcMargin);
125
+
126
+    $(window).resize(function() {
127
+      $("#sip-logitems").css("height", window.innerHeight - 175);
128
+      $("#sip-splash").css("height", window.innerHeight - 135);
129
+    });
130
+
131
+    if (user.Stun != '') {
132
+        var configComp = {
133
+               password        : userSIPPass,
134
+               displayName     : user.Display,
135
+               uri             : 'sip:'+ user.User +'@'+ user.Realm,
136
+               wsServers       : user.WSServer,
137
+               stunServers     : ["stun:"+ user.Stun],
138
+               traceSip        : traceornot,
139
+               log             : { level : 3 },
140
+               registerExpires : 9999999
141
+            };
142
+    } else {
143
+        var configComp = {
144
+               password        : userSIPPass,
145
+               displayName     : user.Display,
146
+               uri             : 'sip:'+ user.User +'@'+ user.Realm,
147
+               wsServers       : user.WSServer,
148
+               traceSip        : traceornot,
149
+               log             : { level : 3 },
150
+               registerExpires : 9999999
151
+            };
152
+    }
153
+
154
+    ctxSip = {
155
+
156
+        config : configComp,
157
+        ringtone     : document.getElementById('ringtone'),
158
+        ringbacktone : document.getElementById('ringbacktone'),
159
+        dtmfTone     : document.getElementById('dtmfTone'),
160
+
161
+        Sessions     : [],
162
+        callTimers   : {},
163
+        callActiveID : null,
164
+        callVolume   : 1,
165
+        Stream       : null,
166
+
167
+        /**
168
+         * Parses a SIP uri and returns a formatted phone number.
169
+         *
170
+         * @param  {string} phone number or uri to format
171
+         * @return {string}       formatted number
172
+         */
173
+        formatPhone : function(phone) {
174
+
175
+            var num;
176
+
177
+            if (phone.indexOf('@')) {
178
+                num =  phone.split('@')[0];
179
+            } else {
180
+                num = phone;
181
+            }
182
+
183
+            num = num.toString().replace(/[^0-9]/g, '');
184
+
185
+            if (num.length === 10) {
186
+                return '(' + num.substr(0, 3) + ') ' + num.substr(3, 3) + '-' + num.substr(6,4);
187
+            } else if (num.length === 11) {
188
+                return '(' + num.substr(1, 3) + ') ' + num.substr(4, 3) + '-' + num.substr(7,4);
189
+            } else {
190
+                return num;
191
+            }
192
+        },
193
+
194
+        // Sound methods
195
+        startRingTone : function() {
196
+            try { ctxSip.ringtone.play(); } catch (e) { }
197
+        },
198
+
199
+        stopRingTone : function() {
200
+            try { ctxSip.ringtone.pause(); } catch (e) { }
201
+        },
202
+
203
+        startRingbackTone : function() {
204
+            try { ctxSip.ringbacktone.play(); } catch (e) { }
205
+        },
206
+
207
+        stopRingbackTone : function() {
208
+            try { ctxSip.ringbacktone.pause(); } catch (e) { }
209
+        },
210
+
211
+        // Genereates a random string to ID a call
212
+        getUniqueID : function() {
213
+            return Math.random().toString(36).substr(2, 9);
214
+        },
215
+
216
+
217
+        newSession : function(newSess) {
218
+
219
+            newSess.displayName = newSess.remoteIdentity.displayName || newSess.remoteIdentity.uri.user;
220
+            newSess.ctxid       = ctxSip.getUniqueID();
221
+
222
+            var status;
223
+
224
+            if (newSess.direction === 'incoming') {
225
+                status = "Incoming: "+ newSess.displayName;
226
+                ctxSip.startRingTone();
227
+
228
+                incomingCallNote();
229
+                changePageTitle();
230
+
231
+            } else {
232
+                status = "Trying: "+ newSess.displayName;
233
+                ctxSip.startRingbackTone();
234
+            }
235
+
236
+            ctxSip.logCall(newSess, 'ringing');
237
+
238
+            ctxSip.setCallSessionStatus(status);
239
+
240
+            // EVENT CALLBACKS
241
+
242
+            newSess.on('progress',function(e) {
243
+                if (e.direction === 'outgoing') {
244
+                    ctxSip.setCallSessionStatus('Calling...');
245
+                }
246
+            });
247
+
248
+            newSess.on('connecting',function(e) {
249
+                if (e.direction === 'outgoing') {
250
+                    ctxSip.setCallSessionStatus('Connecting...');
251
+                }
252
+            });
253
+
254
+
255
+           newSess.on('accepted',function(e) {
256
+
257
+             // If there is another active call, hold it
258
+             if (ctxSip.callActiveID && ctxSip.callActiveID !== newSess.ctxid) {
259
+                 ctxSip.phoneHoldButtonPressed(ctxSip.callActiveID);
260
+             }
261
+
262
+             ctxSip.stopRingbackTone();
263
+             ctxSip.stopRingTone();
264
+             ctxSip.setCallSessionStatus('Answered');
265
+             ctxSip.logCall(newSess, 'answered');
266
+             ctxSip.callActiveID = newSess.ctxid;
267
+           });
268
+
269
+            newSess.on('hold', function(e) {
270
+                ctxSip.callActiveID = null;
271
+                ctxSip.logCall(newSess, 'holding');
272
+            });
273
+
274
+            newSess.on('unhold', function(e) {
275
+                ctxSip.logCall(newSess, 'resumed');
276
+                ctxSip.callActiveID = newSess.ctxid;
277
+            });
278
+
279
+            newSess.on('muted', function(e) {
280
+                ctxSip.Sessions[newSess.ctxid].isMuted = true;
281
+                ctxSip.setCallSessionStatus("Muted");
282
+            });
283
+
284
+            newSess.on('unmuted', function(e) {
285
+                ctxSip.Sessions[newSess.ctxid].isMuted = false;
286
+                ctxSip.setCallSessionStatus("Answered");
287
+            });
288
+
289
+            newSess.on('cancel', function(e) {
290
+                ctxSip.stopRingTone();
291
+                ctxSip.stopRingbackTone();
292
+                ctxSip.setCallSessionStatus("Canceled");
293
+                if (this.direction === 'outgoing') {
294
+                    ctxSip.callActiveID = null;
295
+                    newSess             = null;
296
+                    ctxSip.logCall(this, 'ended');
297
+                }
298
+            });
299
+
300
+            newSess.on('bye', function(e) {
301
+                ctxSip.stopRingTone();
302
+                ctxSip.stopRingbackTone();
303
+                ctxSip.setCallSessionStatus("");
304
+                ctxSip.logCall(newSess, 'ended');
305
+                ctxSip.callActiveID = null;
306
+                newSess             = null;
307
+            });
308
+
309
+            newSess.on('failed',function(e) {
310
+                ctxSip.stopRingTone();
311
+                ctxSip.stopRingbackTone();
312
+                ctxSip.setCallSessionStatus('Terminated');
313
+            });
314
+
315
+            newSess.on('rejected',function(e) {
316
+                ctxSip.stopRingTone();
317
+                ctxSip.stopRingbackTone();
318
+                ctxSip.setCallSessionStatus('Rejected');
319
+                ctxSip.callActiveID = null;
320
+                ctxSip.logCall(this, 'ended');
321
+                newSess             = null;
322
+            });
323
+
324
+            ctxSip.Sessions[newSess.ctxid] = newSess;
325
+
326
+        },
327
+
328
+        // getUser media request refused or device was not present
329
+        getUserMediaFailure : function(e) {
330
+            window.console.error('getUserMedia failed:', e);
331
+            ctxSip.setError(true, 'Media Error.', 'You must allow access to your microphone.  Check the address bar.', true);
332
+        },
333
+
334
+
335
+        getUserMediaSuccess : function(stream) {
336
+            ctxSip.Stream = stream;
337
+        },
338
+
339
+
340
+        /**
341
+         * sets the ui call status field
342
+         *
343
+         * @param {string} status
344
+         */
345
+        setCallSessionStatus : function(status) {
346
+            $('#txtCallStatus').html(status);
347
+        },
348
+
349
+        /**
350
+         * sets the ui connection status field
351
+         *
352
+         * @param {string} status
353
+         */
354
+        setStatus : function(status) {
355
+            $("#txtRegStatus").html('<i class="fa fa-signal"></i> '+status);
356
+        },
357
+
358
+        /**
359
+         * logs a call to localstorage
360
+         *
361
+         * @param  {object} session
362
+         * @param  {string} status Enum 'ringing', 'answered', 'ended', 'holding', 'resumed'
363
+         */
364
+        logCall : function(session, status) {
365
+
366
+            var log = {
367
+                    clid : session.displayName,
368
+                    uri  : session.remoteIdentity.uri.toString(),
369
+                    id   : session.ctxid,
370
+                    time : new Date().getTime()
371
+                },
372
+                calllog = JSON.parse(localStorage.getItem('sipCalls'));
373
+
374
+            if (!calllog) { calllog = {}; }
375
+
376
+            if (!calllog.hasOwnProperty(session.ctxid)) {
377
+                calllog[log.id] = {
378
+                    id    : log.id,
379
+                    clid  : log.clid,
380
+                    uri   : log.uri,
381
+                    start : log.time,
382
+                    flow  : session.direction
383
+                };
384
+            }
385
+
386
+            if (status === 'ended') {
387
+                calllog[log.id].stop = log.time;
388
+            }
389
+
390
+            if (status === 'ended' && calllog[log.id].status === 'ringing') {
391
+                calllog[log.id].status = 'missed';
392
+            } else {
393
+                calllog[log.id].status = status;
394
+            }
395
+
396
+            localStorage.setItem('sipCalls', JSON.stringify(calllog));
397
+            ctxSip.logShow();
398
+        },
399
+
400
+        /**
401
+         * adds a ui item to the call log
402
+         *
403
+         * @param  {object} item log item
404
+         */
405
+        logItem : function(item) {
406
+
407
+            var callActive = (item.status !== 'ended' && item.status !== 'missed'),
408
+                callLength = (item.status !== 'ended')? '<span id="'+item.id+'"></span>': moment.duration(item.stop - item.start).humanize(),
409
+                callClass  = '',
410
+                callIcon,
411
+                i;
412
+
413
+            switch (item.status) {
414
+                case 'ringing'  :
415
+                    callClass = 'list-group-item-success';
416
+                    callIcon  = 'fa-bell';
417
+                    break;
418
+
419
+                case 'missed'   :
420
+                    callClass = 'list-group-item-danger';
421
+                    if (item.flow === "incoming") { callIcon = 'fa-chevron-left'; }
422
+                    if (item.flow === "outgoing") { callIcon = 'fa-chevron-right'; }
423
+                    break;
424
+
425
+                case 'holding'  :
426
+                    callClass = 'list-group-item-warning';
427
+                    callIcon  = 'fa-pause';
428
+                    break;
429
+
430
+                case 'answered' :
431
+                case 'resumed'  :
432
+                    callClass = 'list-group-item-info';
433
+                    callIcon  = 'fa-phone-square';
434
+                    break;
435
+
436
+                case 'ended'  :
437
+                    if (item.flow === "incoming") { callIcon = 'fa-chevron-left'; }
438
+                    if (item.flow === "outgoing") { callIcon = 'fa-chevron-right'; }
439
+                    break;
440
+            }
441
+
442
+
443
+            i  = '<div class="list-group-item sip-logitem clearfix '+callClass+'" data-uri="'+item.uri+'" data-sessionid="'+item.id+'" title="Double Click to Call">';
444
+            i += '<div class="clearfix"><div class="pull-left">';
445
+            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>';
446
+            i += '</div>';
447
+            i += '<div class="pull-right text-right"><em>'+item.clid+'</em><br>' + callLength+'</div></div>';
448
+
449
+            if (callActive) {
450
+                i += '<div class="btn-group btn-group-xs pull-right">';
451
+                if (item.status === 'ringing' && item.flow === 'incoming') {
452
+                    i += '<button class="btn btn-xs btn-success btnCall" title="Call"><i class="fa fa-phone"></i></button>';
453
+                } else {
454
+                    i += '<button class="btn btn-xs btn-primary btnHoldResume" title="Hold"><i class="fa fa-pause"></i></button>';
455
+                    i += '<button class="btn btn-xs btn-info btnTransfer" title="Transfer"><i class="fa fa-random"></i></button>';
456
+                    i += '<button class="btn btn-xs btn-warning btnMute" title="Mute"><i class="fa fa-fw fa-microphone"></i></button>';
457
+                }
458
+                i += '<button class="btn btn-xs btn-danger btnHangUp" title="Hangup"><i class="fa fa-stop"></i></button>';
459
+                i += '</div>';
460
+            }
461
+            i += '</div>';
462
+
463
+            $('#sip-logitems').append(i);
464
+
465
+
466
+            // Start call timer on answer
467
+            if (item.status === 'answered') {
468
+                var tEle = document.getElementById(item.id);
469
+                ctxSip.callTimers[item.id] = new Stopwatch(tEle);
470
+                ctxSip.callTimers[item.id].start();
471
+            }
472
+
473
+            if (callActive && item.status !== 'ringing') {
474
+                ctxSip.callTimers[item.id].start({startTime : item.start});
475
+            }
476
+
477
+            $('#sip-logitems').scrollTop(0);
478
+        },
479
+
480
+        /**
481
+         * updates the call log ui
482
+         */
483
+        logShow : function() {
484
+
485
+            var calllog = JSON.parse(localStorage.getItem('sipCalls')),
486
+            x = [];
487
+
488
+            if (calllog !== null) {
489
+
490
+                $('#sip-splash').addClass('hide');
491
+                $('#sip-log').removeClass('hide');
492
+
493
+                // empty existing logs
494
+                $('#sip-logitems').empty();
495
+
496
+                // JS doesn't guarantee property order so
497
+                // create an array with the start time as
498
+                // the key and sort by that.
499
+
500
+                // Add start time to array
501
+                $.each(calllog, function(k,v) {
502
+                    x.push(v);
503
+                });
504
+
505
+                // sort descending
506
+                x.sort(function(a, b) {
507
+                    return b.start - a.start;
508
+                });
509
+
510
+                $.each(x, function(k, v) {
511
+                    ctxSip.logItem(v);
512
+                });
513
+
514
+            } else {
515
+                $('#sip-splash').removeClass('hide');
516
+                $('#sip-log').addClass('hide');
517
+            }
518
+        },
519
+
520
+        /**
521
+         * removes log items from localstorage and updates the UI
522
+         */
523
+        logClear : function() {
524
+
525
+            localStorage.removeItem('sipCalls');
526
+            ctxSip.logShow();
527
+        },
528
+
529
+        sipCall : function(target) {
530
+
531
+            try {
532
+
533
+	        // Get the name of the SIP provider to send it to Asterisk, so that Asterisk knows what provider
534
+	        // to use for outbound calls, in case multiple phone numbers from different providers are used
535
+	        var callerNumber = $("#fromNumber").val();
536
+
537
+                if (callerNumber.indexOf(":") >= 0) {
538
+	            var shortProv = callerNumber.split(":");
539
+	            var sipProvider = shortProv[0].replace(" ", "");
540
+                    var callFromNumber = shortProv[1].replace(" ", "");
541
+                } else {
542
+	            var sipProvider = 'n/a';
543
+                    var callFromNumber = callerNumber;
544
+                }
545
+
546
+                var s = ctxSip.phone.invite(target, {
547
+                    media : {
548
+                        stream      : ctxSip.Stream,
549
+                        constraints : { audio : true, video : false },
550
+                        render      : { remote: document.getElementById('audioRemote') }
551
+                        // render: { remote: $('#audioRemote').get()[0] }
552
+                        // RTCConstraints : { "optional": [{ 'DtlsSrtpKeyAgreement': 'true'} ]}
553
+                    },
554
+                    extraHeaders    : [ 'X-SipProvider: '+ sipProvider , 'X-CallFromNumber: '+ callFromNumber ]
555
+                });
556
+                s.direction = 'outgoing';
557
+                ctxSip.newSession(s);
558
+
559
+            } catch(e) {
560
+                throw(e);
561
+            }
562
+        },
563
+
564
+        sipTransfer : function(sessionid) {
565
+
566
+                var s  = ctxSip.Sessions[sessionid],
567
+                target = window.prompt('Enter destination number', '');
568
+
569
+                if (target) {
570
+                    ctxSip.setCallSessionStatus('<i>Transfering the call...</i>');
571
+                }
572
+                s.refer(target);
573
+        },
574
+
575
+        sipHangUp : function(sessionid) {
576
+
577
+            var s = ctxSip.Sessions[sessionid];
578
+            // s.terminate();
579
+            if (!s) {
580
+                return;
581
+            } else if (s.startTime) {
582
+                s.bye();
583
+            } else if (s.reject) {
584
+                s.reject();
585
+            } else if (s.cancel) {
586
+                s.cancel();
587
+            }
588
+
589
+        },
590
+
591
+        sipSendDTMF : function(digit) {
592
+
593
+            try { ctxSip.dtmfTone.play(); } catch(e) { }
594
+
595
+            var a = ctxSip.callActiveID;
596
+            if (a) {
597
+                var s = ctxSip.Sessions[a];
598
+                s.dtmf(digit);
599
+            }
600
+        },
601
+
602
+        phoneCallButtonPressed : function(sessionid) {
603
+
604
+                var s  = ctxSip.Sessions[sessionid];
605
+//                target = $("#numDisplay").val();
606
+                var targetinit = $("#numDisplay").val();
607
+
608
+            if (!s) {
609
+
610
+		if (targetinit.trim() !== '') {
611
+		    if (/^[a-zA-Z0-9\+\*\#]+$/.test(targetinit) && targetinit.length < 201) {
612
+
613
+                        var target = targetinit;
614
+//                        $("#numDisplay").val("");
615
+                        ctxSip.sipCall(target);
616
+
617
+                    } else {
618
+	                alert("The phone number or extension that you have tried to dial is not valid. You can only enter numbers, uppercase and lowercase letters, plus signs (+), asterisks (*) and number signs (#) in the 'Recipient' field. Also, the total number of characters cannot exceed 200.");
619
+		    }
620
+                }
621
+
622
+            } else if (s.accept && !s.startTime) {
623
+
624
+                s.accept({
625
+                    media: {
626
+                            stream: ctxSip.Stream,
627
+                            constraints: { audio: true, video: false },
628
+                            render      : { remote: document.getElementById('audioRemote') }
629
+                            // render: { remote: $('#audioRemote').get()[0] }
630
+                            // RTCConstraints : { "optional": [{ 'DtlsSrtpKeyAgreement': 'true'} ]}
631
+                           }
632
+                });
633
+            }
634
+        },
635
+
636
+        phoneMuteButtonPressed : function (sessionid) {
637
+
638
+            var s = ctxSip.Sessions[sessionid];
639
+
640
+            if (!s.isMuted) {
641
+                s.mute();
642
+            } else {
643
+                s.unmute();
644
+            }
645
+        },
646
+
647
+        phoneHoldButtonPressed : function(sessionid) {
648
+
649
+            var s = ctxSip.Sessions[sessionid];
650
+
651
+            if (s.isOnHold().local === true) {
652
+                s.unhold();
653
+            } else {
654
+                s.hold();
655
+            }
656
+        },
657
+
658
+
659
+        setError : function(err, title, msg, closable) {
660
+
661
+            // Show modal if err = true
662
+            if (err === true) {
663
+                $("#mdlError p").html(msg);
664
+                $("#mdlError").modal('show');
665
+
666
+                if (closable) {
667
+                    var b = '<button type="button" class="close" data-dismiss="modal">&times;</button>';
668
+                    $("#mdlError .modal-header").find('button').remove();
669
+                    $("#mdlError .modal-header").prepend(b);
670
+                    $("#mdlError .modal-title").html(title);
671
+                    $("#mdlError").modal({ keyboard : true });
672
+                } else {
673
+                    $("#mdlError .modal-header").find('button').remove();
674
+                    $("#mdlError .modal-title").html(title);
675
+                    $("#mdlError").modal({ keyboard : false });
676
+                }
677
+                $('#numDisplay').prop('disabled', 'disabled');
678
+            } else {
679
+                $('#numDisplay').removeProp('disabled');
680
+                $("#mdlError").modal('hide');
681
+            }
682
+        },
683
+
684
+        /**
685
+         * Tests for a capable browser, return bool, and shows an
686
+         * error modal on fail.
687
+         */
688
+        hasWebRTC : function() {
689
+
690
+            if (navigator.webkitGetUserMedia) {
691
+                return true;
692
+            } else if (navigator.mozGetUserMedia) {
693
+                return true;
694
+            } else if (navigator.getUserMedia) {
695
+                return true;
696
+            } else {
697
+                ctxSip.setError(true, 'Unsupported Browser.', 'Your browser does not support the features required for this phone.');
698
+                window.console.error("WebRTC support not found");
699
+                return false;
700
+            }
701
+        }
702
+    };
703
+
704
+    userSIPPass = '';
705
+    window.opener.sipUserPasswd = '';
706
+
707
+    // Throw an error if the browser can't hack it.
708
+    if (!ctxSip.hasWebRTC()) {
709
+        return true;
710
+    }
711
+
712
+    ctxSip.phone = new SIP.UA(ctxSip.config);
713
+
714
+    ctxSip.phone.on('connected', function(e) {
715
+        ctxSip.setStatus("Connected");
716
+    });
717
+
718
+    ctxSip.phone.on('disconnected', function(e) {
719
+        ctxSip.setStatus("Disconnected");
720
+
721
+        // disable phone
722
+        ctxSip.setError(true, 'Websocket Disconnected.', 'An Error occurred connecting to the websocket.');
723
+
724
+        // remove existing sessions
725
+        $("#sessions > .session").each(function(i, session) {
726
+            ctxSip.removeSession(session, 500);
727
+        });
728
+    });
729
+
730
+    ctxSip.phone.on('registered', function(e) {
731
+
732
+        var closeEditorWarning = function() {
733
+            return 'If you close this window, you will not be able to make or receive calls from your browser.';
734
+        };
735
+
736
+        var closePhone = function() {
737
+            // stop the phone on unload
738
+            localStorage.removeItem('SipTripPhone');
739
+            ctxSip.phone.stop();
740
+        };
741
+
742
+        window.onbeforeunload = closeEditorWarning;
743
+        window.onunload       = closePhone;
744
+
745
+        // This key is set to prevent multiple windows.
746
+        localStorage.setItem('SipTripPhone', 'true');
747
+
748
+        $("#mldError").modal('hide');
749
+        ctxSip.setStatus("Ready");
750
+
751
+        // Get the userMedia and cache the stream
752
+        var audio = document.getElementById('audioRemote');
753
+        var mediaStream = new MediaStream();
754
+        let audioTrack = null;
755
+
756
+        navigator.mediaDevices.getUserMedia({ audio : true, video : false }, ctxSip.getUserMediaSuccess, ctxSip.getUserMediaFailure).then(function(mediaStream) {
757
+
758
+           let audioTracks = mediaStream.getAudioTracks();
759
+           audio.srcObject = mediaStream;
760
+
761
+           if (audioTracks.length) {
762
+               audioTrack = audioTracks[0];
763
+           }
764
+        }).then(function() {
765
+           new Promise(function(resolve) {
766
+               audio.onloadedmetadata = resolve;
767
+           })
768
+        })
769
+
770
+    });
771
+
772
+    ctxSip.phone.on('registrationFailed', function(e) {
773
+        ctxSip.setError(true, 'Registration Error.', 'An error occurred while registering your phone. Please check your settings.');
774
+        ctxSip.setStatus("Error: Registration Failed");
775
+    });
776
+
777
+    ctxSip.phone.on('unregistered', function(e) {
778
+        ctxSip.setError(true, 'Registration Error.', 'An error occurred while registering your phone. Please check your settings.');
779
+        ctxSip.setStatus("Error: Registration Failed");
780
+    });
781
+
782
+    ctxSip.phone.on('invite', function (incomingSession) {
783
+
784
+        var s = incomingSession;
785
+
786
+        s.direction = 'incoming';
787
+        ctxSip.newSession(s);
788
+    });
789
+
790
+    // Auto-focus number input on backspace.
791
+    $('#sipClient').keydown(function(event) {
792
+        if (event.which === 8) {
793
+            $('#numDisplay').focus();
794
+        }
795
+    });
796
+
797
+    $('#numDisplay').keypress(function(e) {
798
+        // Enter pressed? so Dial.
799
+        if (e.which === 13) {
800
+            ctxSip.phoneCallButtonPressed();
801
+        }
802
+    });
803
+
804
+    var clck = 0;
805
+
806
+    $('.digit').click(function(event) {
807
+
808
+     if (event.shiftKey) {
809
+
810
+         clck++;
811
+         event.preventDefault();
812
+         var num = $('#numDisplay').val();
813
+         var dig;
814
+         var diginit = $(this).data('digit').toString().split(',');
815
+         var elct = diginit.length;
816
+
817
+         dig = diginit[clck%elct];
818
+         var numsec = num.slice(0,-1);
819
+         $('#numDisplay').val(numsec+dig);
820
+         ctxSip.sipSendDTMF(dig);
821
+
822
+     } else {
823
+         event.preventDefault();
824
+         var num = $('#numDisplay').val();
825
+         var dig;
826
+         var diginit = $(this).data('digit').toString().split(',');
827
+
828
+         dig = diginit[0];
829
+         clck = 0;
830
+         $('#numDisplay').val(num+dig);
831
+         ctxSip.sipSendDTMF(dig);
832
+       }
833
+
834
+       return false;
835
+
836
+    });
837
+
838
+    $('#phoneUI .dropdown-menu').click(function(e) {
839
+        e.preventDefault();
840
+    });
841
+
842
+    $('#phoneUI').delegate('.btnCall', 'click', function(event) {
843
+        ctxSip.phoneCallButtonPressed();
844
+        // to close the dropdown
845
+        return true;
846
+    });
847
+
848
+    $('.sipLogClear').click(function(event) {
849
+        event.preventDefault();
850
+        ctxSip.logClear();
851
+    });
852
+
853
+    $('#sip-logitems').delegate('.sip-logitem .btnCall', 'click', function(event) {
854
+        var sessionid = $(this).closest('.sip-logitem').data('sessionid');
855
+        ctxSip.phoneCallButtonPressed(sessionid);
856
+        return false;
857
+    });
858
+
859
+    $('#sip-logitems').delegate('.sip-logitem .btnHoldResume', 'click', function(event) {
860
+        var sessionid = $(this).closest('.sip-logitem').data('sessionid');
861
+        ctxSip.phoneHoldButtonPressed(sessionid);
862
+        return false;
863
+    });
864
+
865
+    $('#sip-logitems').delegate('.sip-logitem .btnHangUp', 'click', function(event) {
866
+        var sessionid = $(this).closest('.sip-logitem').data('sessionid');
867
+        ctxSip.sipHangUp(sessionid);
868
+        return false;
869
+    });
870
+
871
+    $('#sip-logitems').delegate('.sip-logitem .btnTransfer', 'click', function(event) {
872
+        var sessionid = $(this).closest('.sip-logitem').data('sessionid');
873
+        ctxSip.sipTransfer(sessionid);
874
+        return false;
875
+    });
876
+
877
+    $('#sip-logitems').delegate('.sip-logitem .btnMute', 'click', function(event) {
878
+        var sessionid = $(this).closest('.sip-logitem').data('sessionid');
879
+        ctxSip.phoneMuteButtonPressed(sessionid);
880
+
881
+        var crtMuteBtn = $(this).closest('.sip-logitem').find('.btnMute');
882
+        if (crtMuteBtn.css("background-color") != "rgb(145, 103, 43)") {
883
+            crtMuteBtn.css("background-color", "rgb(145, 103, 43)");
884
+            crtMuteBtn.prop("title", "Unmute");
885
+        } else {
886
+            crtMuteBtn.css("background-color", "rgb(240, 173, 78)");
887
+            crtMuteBtn.prop("title", "Mute");
888
+        }
889
+        return false;
890
+    });
891
+
892
+    $('#sip-logitems').delegate('.sip-logitem', 'dblclick', function(event) {
893
+        event.preventDefault();
894
+
895
+        var uri = $(this).data('uri');
896
+        $('#numDisplay').val(uri);
897
+        ctxSip.phoneCallButtonPressed();
898
+    });
899
+
900
+    $('#sldVolume').on('change', function() {
901
+
902
+            var v  = $(this).val() / 100,
903
+            btn    = $('#btnVol'),
904
+            icon   = $('#btnVol').find('i'),
905
+            active = ctxSip.callActiveID;
906
+
907
+        // Set the object and media stream volumes
908
+        if (ctxSip.Sessions[active]) {
909
+            ctxSip.Sessions[active].player.volume = v;
910
+            ctxSip.callVolume                     = v;
911
+        }
912
+
913
+        // Set the others
914
+        $('audio').each(function() {
915
+            $(this).get()[0].volume = v;
916
+        });
917
+
918
+        if (v < 0.1) {
919
+            btn.removeClass(function (index, css) {
920
+                   return (css.match (/(^|\s)btn\S+/g) || []).join(' ');
921
+                })
922
+                .addClass('btn btn-sm btn-danger');
923
+            icon.removeClass().addClass('fa fa-fw fa-volume-off');
924
+        } else if (v < 0.8) {
925
+            btn.removeClass(function (index, css) {
926
+                   return (css.match (/(^|\s)btn\S+/g) || []).join(' ');
927
+               }).addClass('btn btn-sm btn-info');
928
+            icon.removeClass().addClass('fa fa-fw fa-volume-down');
929
+        } else {
930
+            btn.removeClass(function (index, css) {
931
+                   return (css.match (/(^|\s)btn\S+/g) || []).join(' ');
932
+               }).addClass('btn btn-sm btn-primary');
933
+            icon.removeClass().addClass('fa fa-fw fa-volume-up');
934
+        }
935
+        return false;
936
+    });
937
+
938
+    // Hide the spalsh after 3 secs.
939
+    setTimeout(function() {
940
+        ctxSip.logShow();
941
+    }, 3000);
942
+
943
+
944
+    /**
945
+     * Stopwatch object used for call timers
946
+     *
947
+     * @param {dom element} elem
948
+     * @param {[object]} options
949
+     */
950
+    var Stopwatch = function(elem, options) {
951
+
952
+        // private functions
953
+        function createTimer() {
954
+            return document.createElement("span");
955
+        }
956
+
957
+        var timer = createTimer(),
958
+            offset,
959
+            clock,
960
+            interval;
961
+
962
+        // default options
963
+        options           = options || {};
964
+        options.delay     = options.delay || 1000;
965
+        options.startTime = options.startTime || Date.now();
966
+
967
+        // append elements
968
+        elem.appendChild(timer);
969
+
970
+        function start() {
971
+            if (!interval) {
972
+                offset   = options.startTime;
973
+                interval = setInterval(update, options.delay);
974
+            }
975
+        }
976
+
977
+        function stop() {
978
+            if (interval) {
979
+                clearInterval(interval);
980
+                interval = null;
981
+            }
982
+        }
983
+
984
+        function reset() {
985
+            clock = 0;
986
+            render();
987
+        }
988
+
989
+        function update() {
990
+            clock += delta();
991
+            render();
992
+        }
993
+
994
+        function render() {
995
+            timer.innerHTML = moment(clock).format('mm:ss');
996
+        }
997
+
998
+        function delta() {
999
+            var now = Date.now(),
1000
+                d   = now - offset;
1001
+
1002
+            offset = now;
1003
+            return d;
1004
+        }
1005
+
1006
+        // initialize
1007
+        reset();
1008
+
1009
+        // public API
1010
+        this.start = start; //function() { start; }
1011
+        this.stop  = stop; //function() { stop; }
1012
+    };
1013
+
1014
+});
Browse code

removed appinfo/info.xml appinfo/signature.json CHANGELOG.txt README.md js/settings.js js/launchphone.js css/style.css phone/scripts/app.js templates/settings.php l10n/en_GB.js l10n/en_GB.json phone/css/ctxSip.css phone/index.html lib/Controller/SphoneController.php lib/Service/SphoneService.php phone/sounds/dtmf.mp3 phone/sounds/incoming.mp3 phone/sounds/outgoing.mp3 img/sip_trip_phone_screenshot.png img/sip_trip_phone_transfer_call.png img/sip_trip_phone_making_calls.png img/sip_trip_phone_initial_screen.png img/sip_trip_phone_dialpad.png

DoubleBastionAdmin authored on 08/01/2024 19:00:48
Showing 1 changed files
1 1
deleted file mode 100644
... ...
@@ -1,937 +0,0 @@
1
-/**
2
- * @copyright 2021 Double Bastion LLC <www.doublebastion.com>
3
- *
4
- * @author Double Bastion LLC
5
- *
6
- * @license GNU AGPL version 3 or any later version
7
- *
8
- * This program is free software; you can redistribute it and/or
9
- * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
10
- * License as published by the Free Software Foundation; either
11
- * version 3 of the License, or any later version.
12
- *
13
- * This program is distributed in the hope that it will be useful,
14
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
- * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
17
- *
18
- * You should have received a copy of the GNU Affero General Public
19
- * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
20
- *
21
- *
22
- *
23
- * This is a modified version of the original file "app.js".
24
- *
25
- * We list below the copyright notice of the ctxSip phone (https://github.com/collecttix/ctxSip)
26
- * which also applies to the original "app.js" file, which was part of it:
27
- *
28
- *
29
- *  The MIT License (MIT)
30
- *
31
- *  Copyright (c) 2014 Collecttix
32
- *
33
- *  Permission is hereby granted, free of charge, to any person obtaining a copy
34
- *  of this software and associated documentation files (the "Software"), to deal
35
- *  in the Software without restriction, including without limitation the rights
36
- *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
37
- *  copies of the Software, and to permit persons to whom the Software is
38
- *  furnished to do so, subject to the following conditions:
39
- *
40
- *  The above copyright notice and this permission notice shall be included in
41
- *  all copies or substantial portions of the Software.
42
- *
43
- *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
44
- *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
45
- *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
46
- *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
47
- *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
48
- *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
49
- *  THE SOFTWARE.
50
- *
51
- */
52
-
53
-
54
-/* globals SIP, user, moment, Stopwatch */
55
-
56
-$(document).ready(function() {
57
-
58
-    var ctxSip;
59
-
60
-    // Show system notifications on incoming calls
61
-    function incomingCallNote() {
62
-       var noticeOptions = { body: "New incoming call !!!", icon: "images/sip_trip_phone_logo.svg" }
63
-       var inComingCallNotification = new Notification("SIP Trip Phone incoming call", noticeOptions);
64
-       inComingCallNotification.onclick = function (event) {
65
-         return;
66
-       }
67
-
68
-       if (document.hasFocus()) {
69
-           return;
70
-       } else { setTimeout(incomingCallNote, 8000); }
71
-    }
72
-
73
-    // Change page title on incoming calls
74
-    function changePageTitle() {
75
-        if ($(document).attr("title") == "SIP Trip Phone") { $(document).prop("title", "New call !!!"); } else { $(document).prop("title", "SIP Trip Phone"); }
76
-        if (document.hasFocus()) {
77
-            $(document).prop("title", "SIP Trip Phone");
78
-            return;
79
-        } else { setTimeout(changePageTitle, 460); }
80
-    }
81
-
82
-    var userSIPPass = window.opener.sipUserPasswd;
83
-
84
-    var user = JSON.parse(localStorage.getItem('SIPCreds'));
85
-
86
-    if (user.Stun != '') {
87
-        var configComp = {
88
-               password        : userSIPPass,
89
-               displayName     : user.Display,
90
-               uri             : 'sip:'+user.User+'@'+user.Realm,
91
-               wsServers       : user.WSServer,
92
-               registerExpires : 9999999,
93
-               traceSip        : true,
94
-               stunServers: ["stun:" + user.Stun],
95
-               log             : {
96
-                   level : 0,
97
-               }
98
-            };
99
-    } else {
100
-        var configComp = {
101
-               password        : userSIPPass,
102
-               displayName     : user.Display,
103
-               uri             : 'sip:'+user.User+'@'+user.Realm,
104
-               wsServers       : user.WSServer,
105
-               registerExpires : 9999999,
106
-               traceSip        : true,
107
-               log             : {
108
-                   level : 0,
109
-               }
110
-            };
111
-    }
112
-
113
-    ctxSip = {
114
-
115
-        config : configComp,
116
-        ringtone     : document.getElementById('ringtone'),
117
-        ringbacktone : document.getElementById('ringbacktone'),
118
-        dtmfTone     : document.getElementById('dtmfTone'),
119
-
120
-        Sessions     : [],
121
-        callTimers   : {},
122
-        callActiveID : null,
123
-        callVolume   : 1,
124
-        Stream       : null,
125
-
126
-        /**
127
-         * Parses a SIP uri and returns a formatted phone number.
128
-         *
129
-         * @param  {string} phone number or uri to format
130
-         * @return {string}       formatted number
131
-         */
132
-        formatPhone : function(phone) {
133
-
134
-            var num;
135
-
136
-            if (phone.indexOf('@')) {
137
-                num =  phone.split('@')[0];
138
-            } else {
139
-                num = phone;
140
-            }
141
-
142
-            num = num.toString().replace(/[^0-9]/g, '');
143
-
144
-            if (num.length === 10) {
145
-                return '(' + num.substr(0, 3) + ') ' + num.substr(3, 3) + '-' + num.substr(6,4);
146
-            } else if (num.length === 11) {
147
-                return '(' + num.substr(1, 3) + ') ' + num.substr(4, 3) + '-' + num.substr(7,4);
148
-            } else {
149
-                return num;
150
-            }
151
-        },
152
-
153
-        // Sound methods
154
-        startRingTone : function() {
155
-            try { ctxSip.ringtone.play(); } catch (e) { }
156
-        },
157
-
158
-        stopRingTone : function() {
159
-            try { ctxSip.ringtone.pause(); } catch (e) { }
160
-        },
161
-
162
-        startRingbackTone : function() {
163
-            try { ctxSip.ringbacktone.play(); } catch (e) { }
164
-        },
165
-
166
-        stopRingbackTone : function() {
167
-            try { ctxSip.ringbacktone.pause(); } catch (e) { }
168
-        },
169
-
170
-        // Genereates a rendom string to ID a call
171
-        getUniqueID : function() {
172
-            return Math.random().toString(36).substr(2, 9);
173
-        },
174
-
175
-
176
-        newSession : function(newSess) {
177
-
178
-            newSess.displayName = newSess.remoteIdentity.displayName || newSess.remoteIdentity.uri.user;
179
-            newSess.ctxid       = ctxSip.getUniqueID();
180
-
181
-            var status;
182
-
183
-            if (newSess.direction === 'incoming') {
184
-                status = "Incoming: "+ newSess.displayName;
185
-                ctxSip.startRingTone();
186
-
187
-                incomingCallNote();
188
-                changePageTitle();
189
-
190
-            } else {
191
-                status = "Trying: "+ newSess.displayName;
192
-                ctxSip.startRingbackTone();
193
-            }
194
-
195
-            ctxSip.logCall(newSess, 'ringing');
196
-
197
-            ctxSip.setCallSessionStatus(status);
198
-
199
-            // EVENT CALLBACKS
200
-
201
-            newSess.on('progress',function(e) {
202
-                if (e.direction === 'outgoing') {
203
-                    ctxSip.setCallSessionStatus('Calling...');
204
-                }
205
-            });
206
-
207
-            newSess.on('connecting',function(e) {
208
-                if (e.direction === 'outgoing') {
209
-                    ctxSip.setCallSessionStatus('Connecting...');
210
-                }
211
-            });
212
-
213
-
214
-           newSess.on('accepted',function(e) {
215
-
216
-             // If there is another active call, hold it
217
-             if (ctxSip.callActiveID && ctxSip.callActiveID !== newSess.ctxid) {
218
-                 ctxSip.phoneHoldButtonPressed(ctxSip.callActiveID);
219
-             }
220
-
221
-             ctxSip.stopRingbackTone();
222
-             ctxSip.stopRingTone();
223
-             ctxSip.setCallSessionStatus('Answered');
224
-             ctxSip.logCall(newSess, 'answered');
225
-             ctxSip.callActiveID = newSess.ctxid;
226
-           });
227
-
228
-            newSess.on('hold', function(e) {
229
-                ctxSip.callActiveID = null;
230
-                ctxSip.logCall(newSess, 'holding');
231
-            });
232
-
233
-            newSess.on('unhold', function(e) {
234
-                ctxSip.logCall(newSess, 'resumed');
235
-                ctxSip.callActiveID = newSess.ctxid;
236
-            });
237
-
238
-            newSess.on('muted', function(e) {
239
-                ctxSip.Sessions[newSess.ctxid].isMuted = true;
240
-                ctxSip.setCallSessionStatus("Muted");
241
-            });
242
-
243
-            newSess.on('unmuted', function(e) {
244
-                ctxSip.Sessions[newSess.ctxid].isMuted = false;
245
-                ctxSip.setCallSessionStatus("Answered");
246
-            });
247
-
248
-            newSess.on('cancel', function(e) {
249
-                ctxSip.stopRingTone();
250
-                ctxSip.stopRingbackTone();
251
-                ctxSip.setCallSessionStatus("Canceled");
252
-                if (this.direction === 'outgoing') {
253
-                    ctxSip.callActiveID = null;
254
-                    newSess             = null;
255
-                    ctxSip.logCall(this, 'ended');
256
-                }
257
-            });
258
-
259
-            newSess.on('bye', function(e) {
260
-                ctxSip.stopRingTone();
261
-                ctxSip.stopRingbackTone();
262
-                ctxSip.setCallSessionStatus("");
263
-                ctxSip.logCall(newSess, 'ended');
264
-                ctxSip.callActiveID = null;
265
-                newSess             = null;
266
-            });
267
-
268
-            newSess.on('failed',function(e) {
269
-                ctxSip.stopRingTone();
270
-                ctxSip.stopRingbackTone();
271
-                ctxSip.setCallSessionStatus('Terminated');
272
-            });
273
-
274
-            newSess.on('rejected',function(e) {
275
-                ctxSip.stopRingTone();
276
-                ctxSip.stopRingbackTone();
277
-                ctxSip.setCallSessionStatus('Rejected');
278
-                ctxSip.callActiveID = null;
279
-                ctxSip.logCall(this, 'ended');
280
-                newSess             = null;
281
-            });
282
-
283
-            ctxSip.Sessions[newSess.ctxid] = newSess;
284
-
285
-        },
286
-
287
-        // getUser media request refused or device was not present
288
-        getUserMediaFailure : function(e) {
289
-            window.console.error('getUserMedia failed:', e);
290
-            ctxSip.setError(true, 'Media Error.', 'You must allow access to your microphone.  Check the address bar.', true);
291
-        },
292
-
293
-
294
-        getUserMediaSuccess : function(stream) {
295
-            ctxSip.Stream = stream;
296
-        },
297
-
298
-
299
-        /**
300
-         * sets the ui call status field
301
-         *
302
-         * @param {string} status
303
-         */
304
-        setCallSessionStatus : function(status) {
305
-            $('#txtCallStatus').html(status);
306
-        },
307
-
308
-        /**
309
-         * sets the ui connection status field
310
-         *
311
-         * @param {string} status
312
-         */
313
-        setStatus : function(status) {
314
-            $("#txtRegStatus").html('<i class="fa fa-signal"></i> '+status);
315
-        },
316
-
317
-        /**
318
-         * logs a call to localstorage
319
-         *
320
-         * @param  {object} session
321
-         * @param  {string} status Enum 'ringing', 'answered', 'ended', 'holding', 'resumed'
322
-         */
323
-        logCall : function(session, status) {
324
-
325
-            var log = {
326
-                    clid : session.displayName,
327
-                    uri  : session.remoteIdentity.uri.toString(),
328
-                    id   : session.ctxid,
329
-                    time : new Date().getTime()
330
-                },
331
-                calllog = JSON.parse(localStorage.getItem('sipCalls'));
332
-
333
-            if (!calllog) { calllog = {}; }
334
-
335
-            if (!calllog.hasOwnProperty(session.ctxid)) {
336
-                calllog[log.id] = {
337
-                    id    : log.id,
338
-                    clid  : log.clid,
339
-                    uri   : log.uri,
340
-                    start : log.time,
341
-                    flow  : session.direction
342
-                };
343
-            }
344
-
345
-            if (status === 'ended') {
346
-                calllog[log.id].stop = log.time;
347
-            }
348
-
349
-            if (status === 'ended' && calllog[log.id].status === 'ringing') {
350
-                calllog[log.id].status = 'missed';
351
-            } else {
352
-                calllog[log.id].status = status;
353
-            }
354
-
355
-            localStorage.setItem('sipCalls', JSON.stringify(calllog));
356
-            ctxSip.logShow();
357
-        },
358
-
359
-        /**
360
-         * adds a ui item to the call log
361
-         *
362
-         * @param  {object} item log item
363
-         */
364
-        logItem : function(item) {
365
-
366
-            var callActive = (item.status !== 'ended' && item.status !== 'missed'),
367
-                callLength = (item.status !== 'ended')? '<span id="'+item.id+'"></span>': moment.duration(item.stop - item.start).humanize(),
368
-                callClass  = '',
369
-                callIcon,
370
-                i;
371
-
372
-            switch (item.status) {
373
-                case 'ringing'  :
374
-                    callClass = 'list-group-item-success';
375
-                    callIcon  = 'fa-bell';
376
-                    break;
377
-
378
-                case 'missed'   :
379
-                    callClass = 'list-group-item-danger';
380
-                    if (item.flow === "incoming") { callIcon = 'fa-chevron-left'; }
381
-                    if (item.flow === "outgoing") { callIcon = 'fa-chevron-right'; }
382
-                    break;
383
-
384
-                case 'holding'  :
385
-                    callClass = 'list-group-item-warning';
386
-                    callIcon  = 'fa-pause';
387
-                    break;
388
-
389
-                case 'answered' :
390
-                case 'resumed'  :
391
-                    callClass = 'list-group-item-info';
392
-                    callIcon  = 'fa-phone-square';
393
-                    break;
394
-
395
-                case 'ended'  :
396
-                    if (item.flow === "incoming") { callIcon = 'fa-chevron-left'; }
397
-                    if (item.flow === "outgoing") { callIcon = 'fa-chevron-right'; }
398
-                    break;
399
-            }
400
-
401
-
402
-            i  = '<div class="list-group-item sip-logitem clearfix '+callClass+'" data-uri="'+item.uri+'" data-sessionid="'+item.id+'" title="Double Click to Call">';
403
-            i += '<div class="clearfix"><div class="pull-left">';
404
-            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>';
405
-            i += '</div>';
406
-            i += '<div class="pull-right text-right"><em>'+item.clid+'</em><br>' + callLength+'</div></div>';
407
-
408
-            if (callActive) {
409
-                i += '<div class="btn-group btn-group-xs pull-right">';
410
-                if (item.status === 'ringing' && item.flow === 'incoming') {
411
-                    i += '<button class="btn btn-xs btn-success btnCall" title="Call"><i class="fa fa-phone"></i></button>';
412
-                } else {
413
-                    i += '<button class="btn btn-xs btn-primary btnHoldResume" title="Hold"><i class="fa fa-pause"></i></button>';
414
-                    i += '<button class="btn btn-xs btn-info btnTransfer" title="Transfer"><i class="fa fa-random"></i></button>';
415
-                    i += '<button class="btn btn-xs btn-warning btnMute" title="Mute"><i class="fa fa-fw fa-microphone"></i></button>';
416
-                }
417
-                i += '<button class="btn btn-xs btn-danger btnHangUp" title="Hangup"><i class="fa fa-stop"></i></button>';
418
-                i += '</div>';
419
-            }
420
-            i += '</div>';
421
-
422
-            $('#sip-logitems').append(i);
423
-
424
-
425
-            // Start call timer on answer
426
-            if (item.status === 'answered') {
427
-                var tEle = document.getElementById(item.id);
428
-                ctxSip.callTimers[item.id] = new Stopwatch(tEle);
429
-                ctxSip.callTimers[item.id].start();
430
-            }
431
-
432
-            if (callActive && item.status !== 'ringing') {
433
-                ctxSip.callTimers[item.id].start({startTime : item.start});
434
-            }
435
-
436
-            $('#sip-logitems').scrollTop(0);
437
-        },
438
-
439
-        /**
440
-         * updates the call log ui
441
-         */
442
-        logShow : function() {
443
-
444
-            var calllog = JSON.parse(localStorage.getItem('sipCalls')),
445
-            x = [];
446
-
447
-            if (calllog !== null) {
448
-
449
-                $('#sip-splash').addClass('hide');
450
-                $('#sip-log').removeClass('hide');
451
-
452
-                // empty existing logs
453
-                $('#sip-logitems').empty();
454
-
455
-                // JS doesn't guarantee property order so
456
-                // create an array with the start time as
457
-                // the key and sort by that.
458
-
459
-                // Add start time to array
460
-                $.each(calllog, function(k,v) {
461
-                    x.push(v);
462
-                });
463
-
464
-                // sort descending
465
-                x.sort(function(a, b) {
466
-                    return b.start - a.start;
467
-                });
468
-
469
-                $.each(x, function(k, v) {
470
-                    ctxSip.logItem(v);
471
-                });
472
-
473
-            } else {
474
-                $('#sip-splash').removeClass('hide');
475
-                $('#sip-log').addClass('hide');
476
-            }
477
-        },
478
-
479
-        /**
480
-         * removes log items from localstorage and updates the UI
481
-         */
482
-        logClear : function() {
483
-
484
-            localStorage.removeItem('sipCalls');
485
-            ctxSip.logShow();
486
-        },
487
-
488
-        sipCall : function(target) {
489
-
490
-            try {
491
-                var s = ctxSip.phone.invite(target, {
492
-                    media : {
493
-                        stream      : ctxSip.Stream,
494
-                        constraints : { audio : true, video : false },
495
-                        render      : { remote: document.getElementById('audioRemote') }
496
-                        // render: { remote: $('#audioRemote').get()[0] }
497
-                        // RTCConstraints : { "optional": [{ 'DtlsSrtpKeyAgreement': 'true'} ]}
498
-                    }
499
-                });
500
-                s.direction = 'outgoing';
501
-                ctxSip.newSession(s);
502
-
503
-            } catch(e) {
504
-                throw(e);
505
-            }
506
-        },
507
-
508
-        sipTransfer : function(sessionid) {
509
-
510
-                var s  = ctxSip.Sessions[sessionid],
511
-                target = window.prompt('Enter destination number', '');
512
-
513
-            ctxSip.setCallSessionStatus('<i>Transfering the call...</i>');
514
-            s.refer(target);
515
-        },
516
-
517
-        sipHangUp : function(sessionid) {
518
-
519
-            var s = ctxSip.Sessions[sessionid];
520
-            // s.terminate();
521
-            if (!s) {
522
-                return;
523
-            } else if (s.startTime) {
524
-                s.bye();
525
-            } else if (s.reject) {
526
-                s.reject();
527
-            } else if (s.cancel) {
528
-                s.cancel();
529
-            }
530
-
531
-        },
532
-
533
-        sipSendDTMF : function(digit) {
534
-
535
-            try { ctxSip.dtmfTone.play(); } catch(e) { }
536
-
537
-            var a = ctxSip.callActiveID;
538
-            if (a) {
539
-                var s = ctxSip.Sessions[a];
540
-                s.dtmf(digit);
541
-            }
542
-        },
543
-
544
-        phoneCallButtonPressed : function(sessionid) {
545
-
546
-                var s  = ctxSip.Sessions[sessionid],
547
-                target = $("#numDisplay").val();
548
-
549
-            if (!s) {
550
-
551
-                $("#numDisplay").val("");
552
-                ctxSip.sipCall(target);
553
-
554
-            } else if (s.accept && !s.startTime) {
555
-
556
-                s.accept({
557
-                    media: {
558
-                            stream: ctxSip.Stream,
559
-                            constraints: { audio: true, video: false },
560
-                            render      : { remote: document.getElementById('audioRemote') }
561
-                            // render: { remote: $('#audioRemote').get()[0] }
562
-                            // RTCConstraints : { "optional": [{ 'DtlsSrtpKeyAgreement': 'true'} ]}
563
-                           }
564
-                });
565
-            }
566
-        },
567
-
568
-        phoneMuteButtonPressed : function (sessionid) {
569
-
570
-            var s = ctxSip.Sessions[sessionid];
571
-
572
-            if (!s.isMuted) {
573
-                s.mute();
574
-            } else {
575
-                s.unmute();
576
-            }
577
-        },
578
-
579
-        phoneHoldButtonPressed : function(sessionid) {
580
-
581
-            var s = ctxSip.Sessions[sessionid];
582
-
583
-            if (s.isOnHold().local === true) {
584
-                s.unhold();
585
-            } else {
586
-                s.hold();
587
-            }
588
-        },
589
-
590
-
591
-        setError : function(err, title, msg, closable) {
592
-
593
-            // Show modal if err = true
594
-            if (err === true) {
595
-                $("#mdlError p").html(msg);
596
-                $("#mdlError").modal('show');
597
-
598
-                if (closable) {
599
-                    var b = '<button type="button" class="close" data-dismiss="modal">&times;</button>';
600
-                    $("#mdlError .modal-header").find('button').remove();
601
-                    $("#mdlError .modal-header").prepend(b);
602
-                    $("#mdlError .modal-title").html(title);
603
-                    $("#mdlError").modal({ keyboard : true });
604
-                } else {
605
-                    $("#mdlError .modal-header").find('button').remove();
606
-                    $("#mdlError .modal-title").html(title);
607
-                    $("#mdlError").modal({ keyboard : false });
608
-                }
609
-                $('#numDisplay').prop('disabled', 'disabled');
610
-            } else {
611
-                $('#numDisplay').removeProp('disabled');
612
-                $("#mdlError").modal('hide');
613
-            }
614
-        },
615
-
616
-        /**
617
-         * Tests for a capable browser, return bool, and shows an
618
-         * error modal on fail.
619
-         */
620
-        hasWebRTC : function() {
621
-
622
-            if (navigator.webkitGetUserMedia) {
623
-                return true;
624
-            } else if (navigator.mozGetUserMedia) {
625
-                return true;
626
-            } else if (navigator.getUserMedia) {
627
-                return true;
628
-            } else {
629
-                ctxSip.setError(true, 'Unsupported Browser.', 'Your browser does not support the features required for this phone.');
630
-                window.console.error("WebRTC support not found");
631
-                return false;
632
-            }
633
-        }
634
-    };
635
-
636
-    userSIPPass = '';
637
-    window.opener.sipUserPasswd = '';
638
-
639
-    // Throw an error if the browser can't hack it.
640
-    if (!ctxSip.hasWebRTC()) {
641
-        return true;
642
-    }
643
-
644
-    ctxSip.phone = new SIP.UA(ctxSip.config);
645
-
646
-    ctxSip.phone.on('connected', function(e) {
647
-        ctxSip.setStatus("Connected");
648
-    });
649
-
650
-    ctxSip.phone.on('disconnected', function(e) {
651
-        ctxSip.setStatus("Disconnected");
652
-
653
-        // disable phone
654
-        ctxSip.setError(true, 'Websocket Disconnected.', 'An Error occurred connecting to the websocket.');
655
-
656
-        // remove existing sessions
657
-        $("#sessions > .session").each(function(i, session) {
658
-            ctxSip.removeSession(session, 500);
659
-        });
660
-    });
661
-
662
-    ctxSip.phone.on('registered', function(e) {
663
-
664
-        var closeEditorWarning = function() {
665
-            return 'If you close this window, you will not be able to make or receive calls from your browser.';
666
-        };
667
-
668
-        var closePhone = function() {
669
-            // stop the phone on unload
670
-            localStorage.removeItem('SipTripPhone');
671
-            ctxSip.phone.stop();
672
-        };
673
-
674
-        window.onbeforeunload = closeEditorWarning;
675
-        window.onunload       = closePhone;
676
-
677
-        // This key is set to prevent multiple windows.
678
-        localStorage.setItem('SipTripPhone', 'true');
679
-
680
-        $("#mldError").modal('hide');
681
-        ctxSip.setStatus("Ready");
682
-
683
-        // Get the userMedia and cache the stream
684
-        var audio = document.getElementById('audioRemote');
685
-        var mediaStream = new MediaStream();
686
-        let audioTrack = null;
687
-
688
-        navigator.mediaDevices.getUserMedia({ audio : true, video : false }, ctxSip.getUserMediaSuccess, ctxSip.getUserMediaFailure).then(function(mediaStream) {
689
-
690
-           let audioTracks = mediaStream.getAudioTracks();
691
-           audio.srcObject = mediaStream;
692
-
693
-           if (audioTracks.length) {
694
-               audioTrack = audioTracks[0];
695
-           }
696
-        }).then(function() {
697
-           new Promise(function(resolve) {
698
-               audio.onloadedmetadata = resolve;
699
-           })
700
-        })
701
-
702
-    });
703
-
704
-    ctxSip.phone.on('registrationFailed', function(e) {
705
-        ctxSip.setError(true, 'Registration Error.', 'An Error occurred registering your phone. Check your settings.');
706
-        ctxSip.setStatus("Error: Registration Failed");
707
-    });
708
-
709
-    ctxSip.phone.on('unregistered', function(e) {
710
-        ctxSip.setError(true, 'Registration Error.', 'An Error occurred registering your phone. Check your settings.');
711
-        ctxSip.setStatus("Error: Registration Failed");
712
-    });
713
-
714
-    ctxSip.phone.on('invite', function (incomingSession) {
715
-
716
-        var s = incomingSession;
717
-
718
-        s.direction = 'incoming';
719
-        ctxSip.newSession(s);
720
-    });
721
-
722
-    // Auto-focus number input on backspace.
723
-    $('#sipClient').keydown(function(event) {
724
-        if (event.which === 8) {
725
-            $('#numDisplay').focus();
726
-        }
727
-    });
728
-
729
-    $('#numDisplay').keypress(function(e) {
730
-        // Enter pressed? so Dial.
731
-        if (e.which === 13) {
732
-            ctxSip.phoneCallButtonPressed();
733
-        }
734
-    });
735
-
736
-    var clck = 0;
737
-
738
-    $('.digit').click(function(event) {
739
-
740
-     if (event.shiftKey) {
741
-
742
-         clck++;
743
-         event.preventDefault();
744
-         var num = $('#numDisplay').val();
745
-         var dig;
746
-         var diginit = $(this).data('digit').toString().split(',');
747
-         var elct = diginit.length;
748
-
749
-         dig = diginit[clck%elct];
750
-         var numsec = num.slice(0,-1);
751
-         $('#numDisplay').val(numsec+dig);
752
-         ctxSip.sipSendDTMF(dig);
753
-
754
-     } else {
755
-         event.preventDefault();
756
-         var num = $('#numDisplay').val();
757
-         var dig;
758
-         var diginit = $(this).data('digit').toString().split(',');
759
-
760
-         dig = diginit[0];
761
-         clck = 0;
762
-         $('#numDisplay').val(num+dig);
763
-         ctxSip.sipSendDTMF(dig);
764
-       }
765
-
766
-       return false;
767
-
768
-    });
769
-
770
-    $('#phoneUI .dropdown-menu').click(function(e) {
771
-        e.preventDefault();
772
-    });
773
-
774
-    $('#phoneUI').delegate('.btnCall', 'click', function(event) {
775
-        ctxSip.phoneCallButtonPressed();
776
-        // to close the dropdown
777
-        return true;
778
-    });
779
-
780
-    $('.sipLogClear').click(function(event) {
781
-        event.preventDefault();
782
-        ctxSip.logClear();
783
-    });
784
-
785
-    $('#sip-logitems').delegate('.sip-logitem .btnCall', 'click', function(event) {
786
-        var sessionid = $(this).closest('.sip-logitem').data('sessionid');
787
-        ctxSip.phoneCallButtonPressed(sessionid);
788
-        return false;
789
-    });
790
-
791
-    $('#sip-logitems').delegate('.sip-logitem .btnHoldResume', 'click', function(event) {
792
-        var sessionid = $(this).closest('.sip-logitem').data('sessionid');
793
-        ctxSip.phoneHoldButtonPressed(sessionid);
794
-        return false;
795
-    });
796
-
797
-    $('#sip-logitems').delegate('.sip-logitem .btnHangUp', 'click', function(event) {
798
-        var sessionid = $(this).closest('.sip-logitem').data('sessionid');
799
-        ctxSip.sipHangUp(sessionid);
800
-        return false;
801
-    });
802
-
803
-    $('#sip-logitems').delegate('.sip-logitem .btnTransfer', 'click', function(event) {
804
-        var sessionid = $(this).closest('.sip-logitem').data('sessionid');
805
-        ctxSip.sipTransfer(sessionid);
806
-        return false;
807
-    });
808
-
809
-    $('#sip-logitems').delegate('.sip-logitem .btnMute', 'click', function(event) {
810
-        var sessionid = $(this).closest('.sip-logitem').data('sessionid');
811
-        ctxSip.phoneMuteButtonPressed(sessionid);
812
-        return false;
813
-    });
814
-
815
-    $('#sip-logitems').delegate('.sip-logitem', 'dblclick', function(event) {
816
-        event.preventDefault();
817
-
818
-        var uri = $(this).data('uri');
819
-        $('#numDisplay').val(uri);
820
-        ctxSip.phoneCallButtonPressed();
821
-    });
822
-
823
-    $('#sldVolume').on('change', function() {
824
-
825
-            var v  = $(this).val() / 100,
826
-            btn    = $('#btnVol'),
827
-            icon   = $('#btnVol').find('i'),
828
-            active = ctxSip.callActiveID;
829
-
830
-        // Set the object and media stream volumes
831
-        if (ctxSip.Sessions[active]) {
832
-            ctxSip.Sessions[active].player.volume = v;
833
-            ctxSip.callVolume                     = v;
834
-        }
835
-
836
-        // Set the others
837
-        $('audio').each(function() {
838
-            $(this).get()[0].volume = v;
839
-        });
840
-
841
-        if (v < 0.1) {
842
-            btn.removeClass(function (index, css) {
843
-                   return (css.match (/(^|\s)btn\S+/g) || []).join(' ');
844
-                })
845
-                .addClass('btn btn-sm btn-danger');
846
-            icon.removeClass().addClass('fa fa-fw fa-volume-off');
847
-        } else if (v < 0.8) {
848
-            btn.removeClass(function (index, css) {
849
-                   return (css.match (/(^|\s)btn\S+/g) || []).join(' ');
850
-               }).addClass('btn btn-sm btn-info');
851
-            icon.removeClass().addClass('fa fa-fw fa-volume-down');
852
-        } else {
853
-            btn.removeClass(function (index, css) {
854
-                   return (css.match (/(^|\s)btn\S+/g) || []).join(' ');
855
-               }).addClass('btn btn-sm btn-primary');
856
-            icon.removeClass().addClass('fa fa-fw fa-volume-up');
857
-        }
858
-        return false;
859
-    });
860
-
861
-    // Hide the spalsh after 3 secs.
862
-    setTimeout(function() {
863
-        ctxSip.logShow();
864
-    }, 3000);
865
-
866
-
867
-    /**
868
-     * Stopwatch object used for call timers
869
-     *
870
-     * @param {dom element} elem
871
-     * @param {[object]} options
872
-     */
873
-    var Stopwatch = function(elem, options) {
874
-
875
-        // private functions
876
-        function createTimer() {
877
-            return document.createElement("span");
878
-        }
879
-
880
-        var timer = createTimer(),
881
-            offset,
882
-            clock,
883
-            interval;
884
-
885
-        // default options
886
-        options           = options || {};
887
-        options.delay     = options.delay || 1000;
888
-        options.startTime = options.startTime || Date.now();
889
-
890
-        // append elements
891
-        elem.appendChild(timer);
892
-
893
-        function start() {
894
-            if (!interval) {
895
-                offset   = options.startTime;
896
-                interval = setInterval(update, options.delay);
897
-            }
898
-        }
899
-
900
-        function stop() {
901
-            if (interval) {
902
-                clearInterval(interval);
903
-                interval = null;
904
-            }
905
-        }
906
-
907
-        function reset() {
908
-            clock = 0;
909
-            render();
910
-        }
911
-
912
-        function update() {
913
-            clock += delta();
914
-            render();
915
-        }
916
-
917
-        function render() {
918
-            timer.innerHTML = moment(clock).format('mm:ss');
919
-        }
920
-
921
-        function delta() {
922
-            var now = Date.now(),
923
-                d   = now - offset;
924
-
925
-            offset = now;
926
-            return d;
927
-        }
928
-
929
-        // initialize
930
-        reset();
931
-
932
-        // public API
933
-        this.start = start; //function() { start; }
934
-        this.stop  = stop; //function() { stop; }
935
-    };
936
-
937
-});
Browse code

added CHANGELOG.txt README.md appinfo/info.xml appinfo/signature.json phone/scripts/app.js templates/settings.php css/style.css

DoubleBastionAdmin authored on 22/09/2022 08:46:22
Showing 1 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,937 @@
1
+/**
2
+ * @copyright 2021 Double Bastion LLC <www.doublebastion.com>
3
+ *
4
+ * @author Double Bastion LLC
5
+ *
6
+ * @license GNU AGPL version 3 or any later version
7
+ *
8
+ * This program is free software; you can redistribute it and/or
9
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
10
+ * License as published by the Free Software Foundation; either
11
+ * version 3 of the License, or any later version.
12
+ *
13
+ * This program is distributed in the hope that it will be useful,
14
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
17
+ *
18
+ * You should have received a copy of the GNU Affero General Public
19
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
20
+ *
21
+ *
22
+ *
23
+ * This is a modified version of the original file "app.js".
24
+ *
25
+ * We list below the copyright notice of the ctxSip phone (https://github.com/collecttix/ctxSip)
26
+ * which also applies to the original "app.js" file, which was part of it:
27
+ *
28
+ *
29
+ *  The MIT License (MIT)
30
+ *
31
+ *  Copyright (c) 2014 Collecttix
32
+ *
33
+ *  Permission is hereby granted, free of charge, to any person obtaining a copy
34
+ *  of this software and associated documentation files (the "Software"), to deal
35
+ *  in the Software without restriction, including without limitation the rights
36
+ *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
37
+ *  copies of the Software, and to permit persons to whom the Software is
38
+ *  furnished to do so, subject to the following conditions:
39
+ *
40
+ *  The above copyright notice and this permission notice shall be included in
41
+ *  all copies or substantial portions of the Software.
42
+ *
43
+ *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
44
+ *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
45
+ *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
46
+ *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
47
+ *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
48
+ *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
49
+ *  THE SOFTWARE.
50
+ *
51
+ */
52
+
53
+
54
+/* globals SIP, user, moment, Stopwatch */
55
+
56
+$(document).ready(function() {
57
+
58
+    var ctxSip;
59
+
60
+    // Show system notifications on incoming calls
61
+    function incomingCallNote() {
62
+       var noticeOptions = { body: "New incoming call !!!", icon: "images/sip_trip_phone_logo.svg" }
63
+       var inComingCallNotification = new Notification("SIP Trip Phone incoming call", noticeOptions);
64
+       inComingCallNotification.onclick = function (event) {
65
+         return;
66
+       }
67
+
68
+       if (document.hasFocus()) {
69
+           return;
70
+       } else { setTimeout(incomingCallNote, 8000); }
71
+    }
72
+
73
+    // Change page title on incoming calls
74
+    function changePageTitle() {
75
+        if ($(document).attr("title") == "SIP Trip Phone") { $(document).prop("title", "New call !!!"); } else { $(document).prop("title", "SIP Trip Phone"); }
76
+        if (document.hasFocus()) {
77
+            $(document).prop("title", "SIP Trip Phone");
78
+            return;
79
+        } else { setTimeout(changePageTitle, 460); }
80
+    }
81
+
82
+    var userSIPPass = window.opener.sipUserPasswd;
83
+
84
+    var user = JSON.parse(localStorage.getItem('SIPCreds'));
85
+
86
+    if (user.Stun != '') {
87
+        var configComp = {
88
+               password        : userSIPPass,
89
+               displayName     : user.Display,
90
+               uri             : 'sip:'+user.User+'@'+user.Realm,
91
+               wsServers       : user.WSServer,
92
+               registerExpires : 9999999,
93
+               traceSip        : true,
94
+               stunServers: ["stun:" + user.Stun],
95
+               log             : {
96
+                   level : 0,
97
+               }
98
+            };
99
+    } else {
100
+        var configComp = {
101
+               password        : userSIPPass,
102
+               displayName     : user.Display,
103
+               uri             : 'sip:'+user.User+'@'+user.Realm,
104
+               wsServers       : user.WSServer,
105
+               registerExpires : 9999999,
106
+               traceSip        : true,
107
+               log             : {
108
+                   level : 0,
109
+               }
110
+            };
111
+    }
112
+
113
+    ctxSip = {
114
+
115
+        config : configComp,
116
+        ringtone     : document.getElementById('ringtone'),
117
+        ringbacktone : document.getElementById('ringbacktone'),
118
+        dtmfTone     : document.getElementById('dtmfTone'),
119
+
120
+        Sessions     : [],
121
+        callTimers   : {},
122
+        callActiveID : null,
123
+        callVolume   : 1,
124
+        Stream       : null,
125
+
126
+        /**
127
+         * Parses a SIP uri and returns a formatted phone number.
128
+         *
129
+         * @param  {string} phone number or uri to format
130
+         * @return {string}       formatted number
131
+         */
132
+        formatPhone : function(phone) {
133
+
134
+            var num;
135
+
136
+            if (phone.indexOf('@')) {
137
+                num =  phone.split('@')[0];
138
+            } else {
139
+                num = phone;
140
+            }
141
+
142
+            num = num.toString().replace(/[^0-9]/g, '');
143
+
144
+            if (num.length === 10) {
145
+                return '(' + num.substr(0, 3) + ') ' + num.substr(3, 3) + '-' + num.substr(6,4);
146
+            } else if (num.length === 11) {
147
+                return '(' + num.substr(1, 3) + ') ' + num.substr(4, 3) + '-' + num.substr(7,4);
148
+            } else {
149
+                return num;
150
+            }
151
+        },
152
+
153
+        // Sound methods
154
+        startRingTone : function() {
155
+            try { ctxSip.ringtone.play(); } catch (e) { }
156
+        },
157
+
158
+        stopRingTone : function() {
159
+            try { ctxSip.ringtone.pause(); } catch (e) { }
160
+        },
161
+
162
+        startRingbackTone : function() {
163
+            try { ctxSip.ringbacktone.play(); } catch (e) { }
164
+        },
165
+
166
+        stopRingbackTone : function() {
167
+            try { ctxSip.ringbacktone.pause(); } catch (e) { }
168
+        },
169
+
170
+        // Genereates a rendom string to ID a call
171
+        getUniqueID : function() {
172
+            return Math.random().toString(36).substr(2, 9);
173
+        },
174
+
175
+
176
+        newSession : function(newSess) {
177
+
178
+            newSess.displayName = newSess.remoteIdentity.displayName || newSess.remoteIdentity.uri.user;
179
+            newSess.ctxid       = ctxSip.getUniqueID();
180
+
181
+            var status;
182
+
183
+            if (newSess.direction === 'incoming') {
184
+                status = "Incoming: "+ newSess.displayName;
185
+                ctxSip.startRingTone();
186
+
187
+                incomingCallNote();
188
+                changePageTitle();
189
+
190
+            } else {
191
+                status = "Trying: "+ newSess.displayName;
192
+                ctxSip.startRingbackTone();
193
+            }
194
+
195
+            ctxSip.logCall(newSess, 'ringing');
196
+
197
+            ctxSip.setCallSessionStatus(status);
198
+
199
+            // EVENT CALLBACKS
200
+
201
+            newSess.on('progress',function(e) {
202
+                if (e.direction === 'outgoing') {
203
+                    ctxSip.setCallSessionStatus('Calling...');
204
+                }
205
+            });
206
+
207
+            newSess.on('connecting',function(e) {
208
+                if (e.direction === 'outgoing') {
209
+                    ctxSip.setCallSessionStatus('Connecting...');
210
+                }
211
+            });
212
+
213
+
214
+           newSess.on('accepted',function(e) {
215
+
216
+             // If there is another active call, hold it
217
+             if (ctxSip.callActiveID && ctxSip.callActiveID !== newSess.ctxid) {
218
+                 ctxSip.phoneHoldButtonPressed(ctxSip.callActiveID);
219
+             }
220
+
221
+             ctxSip.stopRingbackTone();
222
+             ctxSip.stopRingTone();
223
+             ctxSip.setCallSessionStatus('Answered');
224
+             ctxSip.logCall(newSess, 'answered');
225
+             ctxSip.callActiveID = newSess.ctxid;
226
+           });
227
+
228
+            newSess.on('hold', function(e) {
229
+                ctxSip.callActiveID = null;
230
+                ctxSip.logCall(newSess, 'holding');
231
+            });
232
+
233
+            newSess.on('unhold', function(e) {
234
+                ctxSip.logCall(newSess, 'resumed');
235
+                ctxSip.callActiveID = newSess.ctxid;
236
+            });
237
+
238
+            newSess.on('muted', function(e) {
239
+                ctxSip.Sessions[newSess.ctxid].isMuted = true;
240
+                ctxSip.setCallSessionStatus("Muted");
241
+            });
242
+
243
+            newSess.on('unmuted', function(e) {
244
+                ctxSip.Sessions[newSess.ctxid].isMuted = false;
245
+                ctxSip.setCallSessionStatus("Answered");
246
+            });
247
+
248
+            newSess.on('cancel', function(e) {
249
+                ctxSip.stopRingTone();
250
+                ctxSip.stopRingbackTone();
251
+                ctxSip.setCallSessionStatus("Canceled");
252
+                if (this.direction === 'outgoing') {
253
+                    ctxSip.callActiveID = null;
254
+                    newSess             = null;
255
+                    ctxSip.logCall(this, 'ended');
256
+                }
257
+            });
258
+
259
+            newSess.on('bye', function(e) {
260
+                ctxSip.stopRingTone();
261
+                ctxSip.stopRingbackTone();
262
+                ctxSip.setCallSessionStatus("");
263
+                ctxSip.logCall(newSess, 'ended');
264
+                ctxSip.callActiveID = null;
265
+                newSess             = null;
266
+            });
267
+
268
+            newSess.on('failed',function(e) {
269
+                ctxSip.stopRingTone();
270
+                ctxSip.stopRingbackTone();
271
+                ctxSip.setCallSessionStatus('Terminated');
272
+            });
273
+
274
+            newSess.on('rejected',function(e) {
275
+                ctxSip.stopRingTone();
276
+                ctxSip.stopRingbackTone();
277
+                ctxSip.setCallSessionStatus('Rejected');
278
+                ctxSip.callActiveID = null;
279
+                ctxSip.logCall(this, 'ended');
280
+                newSess             = null;
281
+            });
282
+
283
+            ctxSip.Sessions[newSess.ctxid] = newSess;
284
+
285
+        },
286
+
287
+        // getUser media request refused or device was not present
288
+        getUserMediaFailure : function(e) {
289
+            window.console.error('getUserMedia failed:', e);
290
+            ctxSip.setError(true, 'Media Error.', 'You must allow access to your microphone.  Check the address bar.', true);
291
+        },
292
+
293
+
294
+        getUserMediaSuccess : function(stream) {
295
+            ctxSip.Stream = stream;
296
+        },
297
+
298
+
299
+        /**
300
+         * sets the ui call status field
301
+         *
302
+         * @param {string} status
303
+         */
304
+        setCallSessionStatus : function(status) {
305
+            $('#txtCallStatus').html(status);
306
+        },
307
+
308
+        /**
309
+         * sets the ui connection status field
310
+         *
311
+         * @param {string} status
312
+         */
313
+        setStatus : function(status) {
314
+            $("#txtRegStatus").html('<i class="fa fa-signal"></i> '+status);
315
+        },
316
+
317
+        /**
318
+         * logs a call to localstorage
319
+         *
320
+         * @param  {object} session
321
+         * @param  {string} status Enum 'ringing', 'answered', 'ended', 'holding', 'resumed'
322
+         */
323
+        logCall : function(session, status) {
324
+
325
+            var log = {
326
+                    clid : session.displayName,
327
+                    uri  : session.remoteIdentity.uri.toString(),
328
+                    id   : session.ctxid,
329
+                    time : new Date().getTime()
330
+                },
331
+                calllog = JSON.parse(localStorage.getItem('sipCalls'));
332
+
333
+            if (!calllog) { calllog = {}; }
334
+
335
+            if (!calllog.hasOwnProperty(session.ctxid)) {
336
+                calllog[log.id] = {
337
+                    id    : log.id,
338
+                    clid  : log.clid,
339
+                    uri   : log.uri,
340
+                    start : log.time,
341
+                    flow  : session.direction
342
+                };
343
+            }
344
+
345
+            if (status === 'ended') {
346
+                calllog[log.id].stop = log.time;
347
+            }
348
+
349
+            if (status === 'ended' && calllog[log.id].status === 'ringing') {
350
+                calllog[log.id].status = 'missed';
351
+            } else {
352
+                calllog[log.id].status = status;
353
+            }
354
+
355
+            localStorage.setItem('sipCalls', JSON.stringify(calllog));
356
+            ctxSip.logShow();
357
+        },
358
+
359
+        /**
360
+         * adds a ui item to the call log
361
+         *
362
+         * @param  {object} item log item
363
+         */
364
+        logItem : function(item) {
365
+
366
+            var callActive = (item.status !== 'ended' && item.status !== 'missed'),
367
+                callLength = (item.status !== 'ended')? '<span id="'+item.id+'"></span>': moment.duration(item.stop - item.start).humanize(),
368
+                callClass  = '',
369
+                callIcon,
370
+                i;
371
+
372
+            switch (item.status) {
373
+                case 'ringing'  :
374
+                    callClass = 'list-group-item-success';
375
+                    callIcon  = 'fa-bell';
376
+                    break;
377
+
378
+                case 'missed'   :
379
+                    callClass = 'list-group-item-danger';
380
+                    if (item.flow === "incoming") { callIcon = 'fa-chevron-left'; }
381
+                    if (item.flow === "outgoing") { callIcon = 'fa-chevron-right'; }
382
+                    break;
383
+
384
+                case 'holding'  :
385
+                    callClass = 'list-group-item-warning';
386
+                    callIcon  = 'fa-pause';
387
+                    break;
388
+
389
+                case 'answered' :
390
+                case 'resumed'  :
391
+                    callClass = 'list-group-item-info';
392
+                    callIcon  = 'fa-phone-square';
393
+                    break;
394
+
395
+                case 'ended'  :
396
+                    if (item.flow === "incoming") { callIcon = 'fa-chevron-left'; }
397
+                    if (item.flow === "outgoing") { callIcon = 'fa-chevron-right'; }
398
+                    break;
399
+            }
400
+
401
+
402
+            i  = '<div class="list-group-item sip-logitem clearfix '+callClass+'" data-uri="'+item.uri+'" data-sessionid="'+item.id+'" title="Double Click to Call">';
403
+            i += '<div class="clearfix"><div class="pull-left">';
404
+            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>';
405
+            i += '</div>';
406
+            i += '<div class="pull-right text-right"><em>'+item.clid+'</em><br>' + callLength+'</div></div>';
407
+
408
+            if (callActive) {
409
+                i += '<div class="btn-group btn-group-xs pull-right">';
410
+                if (item.status === 'ringing' && item.flow === 'incoming') {
411
+                    i += '<button class="btn btn-xs btn-success btnCall" title="Call"><i class="fa fa-phone"></i></button>';
412
+                } else {
413
+                    i += '<button class="btn btn-xs btn-primary btnHoldResume" title="Hold"><i class="fa fa-pause"></i></button>';
414
+                    i += '<button class="btn btn-xs btn-info btnTransfer" title="Transfer"><i class="fa fa-random"></i></button>';
415
+                    i += '<button class="btn btn-xs btn-warning btnMute" title="Mute"><i class="fa fa-fw fa-microphone"></i></button>';
416
+                }
417
+                i += '<button class="btn btn-xs btn-danger btnHangUp" title="Hangup"><i class="fa fa-stop"></i></button>';
418
+                i += '</div>';
419
+            }
420
+            i += '</div>';
421
+
422
+            $('#sip-logitems').append(i);
423
+
424
+
425
+            // Start call timer on answer
426
+            if (item.status === 'answered') {
427
+                var tEle = document.getElementById(item.id);
428
+                ctxSip.callTimers[item.id] = new Stopwatch(tEle);
429
+                ctxSip.callTimers[item.id].start();
430
+            }
431
+
432
+            if (callActive && item.status !== 'ringing') {
433
+                ctxSip.callTimers[item.id].start({startTime : item.start});
434
+            }
435
+
436
+            $('#sip-logitems').scrollTop(0);
437
+        },
438
+
439
+        /**
440
+         * updates the call log ui
441
+         */
442
+        logShow : function() {
443
+
444
+            var calllog = JSON.parse(localStorage.getItem('sipCalls')),
445
+            x = [];
446
+
447
+            if (calllog !== null) {
448
+
449
+                $('#sip-splash').addClass('hide');
450
+                $('#sip-log').removeClass('hide');
451
+
452
+                // empty existing logs
453
+                $('#sip-logitems').empty();
454
+
455
+                // JS doesn't guarantee property order so
456
+                // create an array with the start time as
457
+                // the key and sort by that.
458
+
459
+                // Add start time to array
460
+                $.each(calllog, function(k,v) {
461
+                    x.push(v);
462
+                });
463
+
464
+                // sort descending
465
+                x.sort(function(a, b) {
466
+                    return b.start - a.start;
467
+                });
468
+
469
+                $.each(x, function(k, v) {
470
+                    ctxSip.logItem(v);
471
+                });
472
+
473
+            } else {
474
+                $('#sip-splash').removeClass('hide');
475
+                $('#sip-log').addClass('hide');
476
+            }
477
+        },
478
+
479
+        /**
480
+         * removes log items from localstorage and updates the UI
481
+         */
482
+        logClear : function() {
483
+
484
+            localStorage.removeItem('sipCalls');
485
+            ctxSip.logShow();
486
+        },
487
+
488
+        sipCall : function(target) {
489
+
490
+            try {
491
+                var s = ctxSip.phone.invite(target, {
492
+                    media : {
493
+                        stream      : ctxSip.Stream,
494
+                        constraints : { audio : true, video : false },
495
+                        render      : { remote: document.getElementById('audioRemote') }
496
+                        // render: { remote: $('#audioRemote').get()[0] }
497
+                        // RTCConstraints : { "optional": [{ 'DtlsSrtpKeyAgreement': 'true'} ]}
498
+                    }
499
+                });
500
+                s.direction = 'outgoing';
501
+                ctxSip.newSession(s);
502
+
503
+            } catch(e) {
504
+                throw(e);
505
+            }
506
+        },
507
+
508
+        sipTransfer : function(sessionid) {
509
+
510
+                var s  = ctxSip.Sessions[sessionid],
511
+                target = window.prompt('Enter destination number', '');
512
+
513
+            ctxSip.setCallSessionStatus('<i>Transfering the call...</i>');
514
+            s.refer(target);
515
+        },
516
+
517
+        sipHangUp : function(sessionid) {
518
+
519
+            var s = ctxSip.Sessions[sessionid];
520
+            // s.terminate();
521
+            if (!s) {
522
+                return;
523
+            } else if (s.startTime) {
524
+                s.bye();
525
+            } else if (s.reject) {
526
+                s.reject();
527
+            } else if (s.cancel) {
528
+                s.cancel();
529
+            }
530
+
531
+        },
532
+
533
+        sipSendDTMF : function(digit) {
534
+
535
+            try { ctxSip.dtmfTone.play(); } catch(e) { }
536
+
537
+            var a = ctxSip.callActiveID;
538
+            if (a) {
539
+                var s = ctxSip.Sessions[a];
540
+                s.dtmf(digit);
541
+            }
542
+        },
543
+
544
+        phoneCallButtonPressed : function(sessionid) {
545
+
546
+                var s  = ctxSip.Sessions[sessionid],
547
+                target = $("#numDisplay").val();
548
+
549
+            if (!s) {
550
+
551
+                $("#numDisplay").val("");
552
+                ctxSip.sipCall(target);
553
+
554
+            } else if (s.accept && !s.startTime) {
555
+
556
+                s.accept({
557
+                    media: {
558
+                            stream: ctxSip.Stream,
559
+                            constraints: { audio: true, video: false },
560
+                            render      : { remote: document.getElementById('audioRemote') }
561
+                            // render: { remote: $('#audioRemote').get()[0] }
562
+                            // RTCConstraints : { "optional": [{ 'DtlsSrtpKeyAgreement': 'true'} ]}
563
+                           }
564
+                });
565
+            }
566
+        },
567
+
568
+        phoneMuteButtonPressed : function (sessionid) {
569
+
570
+            var s = ctxSip.Sessions[sessionid];
571
+
572
+            if (!s.isMuted) {
573
+                s.mute();
574
+            } else {
575
+                s.unmute();
576
+            }
577
+        },
578
+
579
+        phoneHoldButtonPressed : function(sessionid) {
580
+
581
+            var s = ctxSip.Sessions[sessionid];
582
+
583
+            if (s.isOnHold().local === true) {
584
+                s.unhold();
585
+            } else {
586
+                s.hold();
587
+            }
588
+        },
589
+
590
+
591
+        setError : function(err, title, msg, closable) {
592
+
593
+            // Show modal if err = true
594
+            if (err === true) {
595
+                $("#mdlError p").html(msg);
596
+                $("#mdlError").modal('show');
597
+
598
+                if (closable) {
599
+                    var b = '<button type="button" class="close" data-dismiss="modal">&times;</button>';
600
+                    $("#mdlError .modal-header").find('button').remove();
601
+                    $("#mdlError .modal-header").prepend(b);
602
+                    $("#mdlError .modal-title").html(title);
603
+                    $("#mdlError").modal({ keyboard : true });
604
+                } else {
605
+                    $("#mdlError .modal-header").find('button').remove();
606
+                    $("#mdlError .modal-title").html(title);
607
+                    $("#mdlError").modal({ keyboard : false });
608
+                }
609
+                $('#numDisplay').prop('disabled', 'disabled');
610
+            } else {
611
+                $('#numDisplay').removeProp('disabled');
612
+                $("#mdlError").modal('hide');
613
+            }
614
+        },
615
+
616
+        /**
617
+         * Tests for a capable browser, return bool, and shows an
618
+         * error modal on fail.
619
+         */
620
+        hasWebRTC : function() {
621
+
622
+            if (navigator.webkitGetUserMedia) {
623
+                return true;
624
+            } else if (navigator.mozGetUserMedia) {
625
+                return true;
626
+            } else if (navigator.getUserMedia) {
627
+                return true;
628
+            } else {
629
+                ctxSip.setError(true, 'Unsupported Browser.', 'Your browser does not support the features required for this phone.');
630
+                window.console.error("WebRTC support not found");
631
+                return false;
632
+            }
633
+        }
634
+    };
635
+
636
+    userSIPPass = '';
637
+    window.opener.sipUserPasswd = '';
638
+
639
+    // Throw an error if the browser can't hack it.
640
+    if (!ctxSip.hasWebRTC()) {
641
+        return true;
642
+    }
643
+
644
+    ctxSip.phone = new SIP.UA(ctxSip.config);
645
+
646
+    ctxSip.phone.on('connected', function(e) {
647
+        ctxSip.setStatus("Connected");
648
+    });
649
+
650
+    ctxSip.phone.on('disconnected', function(e) {
651
+        ctxSip.setStatus("Disconnected");
652
+
653
+        // disable phone
654
+        ctxSip.setError(true, 'Websocket Disconnected.', 'An Error occurred connecting to the websocket.');
655
+
656
+        // remove existing sessions
657
+        $("#sessions > .session").each(function(i, session) {
658
+            ctxSip.removeSession(session, 500);
659
+        });
660
+    });
661
+
662
+    ctxSip.phone.on('registered', function(e) {
663
+
664
+        var closeEditorWarning = function() {
665
+            return 'If you close this window, you will not be able to make or receive calls from your browser.';
666
+        };
667
+
668
+        var closePhone = function() {
669
+            // stop the phone on unload
670
+            localStorage.removeItem('SipTripPhone');
671
+            ctxSip.phone.stop();
672
+        };
673
+
674
+        window.onbeforeunload = closeEditorWarning;
675
+        window.onunload       = closePhone;
676
+
677
+        // This key is set to prevent multiple windows.
678
+        localStorage.setItem('SipTripPhone', 'true');
679
+
680
+        $("#mldError").modal('hide');
681
+        ctxSip.setStatus("Ready");
682
+
683
+        // Get the userMedia and cache the stream
684
+        var audio = document.getElementById('audioRemote');
685
+        var mediaStream = new MediaStream();
686
+        let audioTrack = null;
687
+
688
+        navigator.mediaDevices.getUserMedia({ audio : true, video : false }, ctxSip.getUserMediaSuccess, ctxSip.getUserMediaFailure).then(function(mediaStream) {
689
+
690
+           let audioTracks = mediaStream.getAudioTracks();
691
+           audio.srcObject = mediaStream;
692
+
693
+           if (audioTracks.length) {
694
+               audioTrack = audioTracks[0];
695
+           }
696
+        }).then(function() {
697
+           new Promise(function(resolve) {
698
+               audio.onloadedmetadata = resolve;
699
+           })
700
+        })
701
+
702
+    });
703
+
704
+    ctxSip.phone.on('registrationFailed', function(e) {
705
+        ctxSip.setError(true, 'Registration Error.', 'An Error occurred registering your phone. Check your settings.');
706
+        ctxSip.setStatus("Error: Registration Failed");
707
+    });
708
+
709
+    ctxSip.phone.on('unregistered', function(e) {
710
+        ctxSip.setError(true, 'Registration Error.', 'An Error occurred registering your phone. Check your settings.');
711
+        ctxSip.setStatus("Error: Registration Failed");
712
+    });
713
+
714
+    ctxSip.phone.on('invite', function (incomingSession) {
715
+
716
+        var s = incomingSession;
717
+
718
+        s.direction = 'incoming';
719
+        ctxSip.newSession(s);
720
+    });
721
+
722
+    // Auto-focus number input on backspace.
723
+    $('#sipClient').keydown(function(event) {
724
+        if (event.which === 8) {
725
+            $('#numDisplay').focus();
726
+        }
727
+    });
728
+
729
+    $('#numDisplay').keypress(function(e) {
730
+        // Enter pressed? so Dial.
731
+        if (e.which === 13) {
732
+            ctxSip.phoneCallButtonPressed();
733
+        }
734
+    });
735
+
736
+    var clck = 0;
737
+
738
+    $('.digit').click(function(event) {
739
+
740
+     if (event.shiftKey) {
741
+
742
+         clck++;
743
+         event.preventDefault();
744
+         var num = $('#numDisplay').val();
745
+         var dig;
746
+         var diginit = $(this).data('digit').toString().split(',');
747
+         var elct = diginit.length;
748
+
749
+         dig = diginit[clck%elct];
750
+         var numsec = num.slice(0,-1);
751
+         $('#numDisplay').val(numsec+dig);
752
+         ctxSip.sipSendDTMF(dig);
753
+
754
+     } else {
755
+         event.preventDefault();
756
+         var num = $('#numDisplay').val();
757
+         var dig;
758
+         var diginit = $(this).data('digit').toString().split(',');
759
+
760
+         dig = diginit[0];
761
+         clck = 0;
762
+         $('#numDisplay').val(num+dig);
763
+         ctxSip.sipSendDTMF(dig);
764
+       }
765
+
766
+       return false;
767
+
768
+    });
769
+
770
+    $('#phoneUI .dropdown-menu').click(function(e) {
771
+        e.preventDefault();
772
+    });
773
+
774
+    $('#phoneUI').delegate('.btnCall', 'click', function(event) {
775
+        ctxSip.phoneCallButtonPressed();
776
+        // to close the dropdown
777
+        return true;
778
+    });
779
+
780
+    $('.sipLogClear').click(function(event) {
781
+        event.preventDefault();
782
+        ctxSip.logClear();
783
+    });
784
+
785
+    $('#sip-logitems').delegate('.sip-logitem .btnCall', 'click', function(event) {
786
+        var sessionid = $(this).closest('.sip-logitem').data('sessionid');
787
+        ctxSip.phoneCallButtonPressed(sessionid);
788
+        return false;
789
+    });
790
+
791
+    $('#sip-logitems').delegate('.sip-logitem .btnHoldResume', 'click', function(event) {
792
+        var sessionid = $(this).closest('.sip-logitem').data('sessionid');
793
+        ctxSip.phoneHoldButtonPressed(sessionid);
794
+        return false;
795
+    });
796
+
797
+    $('#sip-logitems').delegate('.sip-logitem .btnHangUp', 'click', function(event) {
798
+        var sessionid = $(this).closest('.sip-logitem').data('sessionid');
799
+        ctxSip.sipHangUp(sessionid);
800
+        return false;
801
+    });
802
+
803
+    $('#sip-logitems').delegate('.sip-logitem .btnTransfer', 'click', function(event) {
804
+        var sessionid = $(this).closest('.sip-logitem').data('sessionid');
805
+        ctxSip.sipTransfer(sessionid);
806
+        return false;
807
+    });
808
+
809
+    $('#sip-logitems').delegate('.sip-logitem .btnMute', 'click', function(event) {
810
+        var sessionid = $(this).closest('.sip-logitem').data('sessionid');
811
+        ctxSip.phoneMuteButtonPressed(sessionid);
812
+        return false;
813
+    });
814
+
815
+    $('#sip-logitems').delegate('.sip-logitem', 'dblclick', function(event) {
816
+        event.preventDefault();
817
+
818
+        var uri = $(this).data('uri');
819
+        $('#numDisplay').val(uri);
820
+        ctxSip.phoneCallButtonPressed();
821
+    });
822
+
823
+    $('#sldVolume').on('change', function() {
824
+
825
+            var v  = $(this).val() / 100,
826
+            btn    = $('#btnVol'),
827
+            icon   = $('#btnVol').find('i'),
828
+            active = ctxSip.callActiveID;
829
+
830
+        // Set the object and media stream volumes
831
+        if (ctxSip.Sessions[active]) {
832
+            ctxSip.Sessions[active].player.volume = v;
833
+            ctxSip.callVolume                     = v;
834
+        }
835
+
836
+        // Set the others
837
+        $('audio').each(function() {
838
+            $(this).get()[0].volume = v;
839
+        });
840
+
841
+        if (v < 0.1) {
842
+            btn.removeClass(function (index, css) {
843
+                   return (css.match (/(^|\s)btn\S+/g) || []).join(' ');
844
+                })
845
+                .addClass('btn btn-sm btn-danger');
846
+            icon.removeClass().addClass('fa fa-fw fa-volume-off');
847
+        } else if (v < 0.8) {
848
+            btn.removeClass(function (index, css) {
849
+                   return (css.match (/(^|\s)btn\S+/g) || []).join(' ');
850
+               }).addClass('btn btn-sm btn-info');
851
+            icon.removeClass().addClass('fa fa-fw fa-volume-down');
852
+        } else {
853
+            btn.removeClass(function (index, css) {
854
+                   return (css.match (/(^|\s)btn\S+/g) || []).join(' ');
855
+               }).addClass('btn btn-sm btn-primary');
856
+            icon.removeClass().addClass('fa fa-fw fa-volume-up');
857
+        }
858
+        return false;
859
+    });
860
+
861
+    // Hide the spalsh after 3 secs.
862
+    setTimeout(function() {
863
+        ctxSip.logShow();
864
+    }, 3000);
865
+
866
+
867
+    /**
868
+     * Stopwatch object used for call timers
869
+     *
870
+     * @param {dom element} elem
871
+     * @param {[object]} options
872
+     */
873
+    var Stopwatch = function(elem, options) {
874
+
875
+        // private functions
876
+        function createTimer() {
877
+            return document.createElement("span");
878
+        }
879
+
880
+        var timer = createTimer(),
881
+            offset,
882
+            clock,
883
+            interval;
884
+
885
+        // default options
886
+        options           = options || {};
887
+        options.delay     = options.delay || 1000;
888
+        options.startTime = options.startTime || Date.now();
889
+
890
+        // append elements
891
+        elem.appendChild(timer);
892
+
893
+        function start() {
894
+            if (!interval) {
895
+                offset   = options.startTime;
896
+                interval = setInterval(update, options.delay);
897
+            }
898
+        }
899
+
900
+        function stop() {
901
+            if (interval) {
902
+                clearInterval(interval);
903
+                interval = null;
904
+            }
905
+        }
906
+
907
+        function reset() {
908
+            clock = 0;
909
+            render();
910
+        }
911
+
912
+        function update() {
913
+            clock += delta();
914
+            render();
915
+        }
916
+
917
+        function render() {
918
+            timer.innerHTML = moment(clock).format('mm:ss');
919
+        }
920
+
921
+        function delta() {
922
+            var now = Date.now(),
923
+                d   = now - offset;
924
+
925
+            offset = now;
926
+            return d;
927
+        }
928
+
929
+        // initialize
930
+        reset();
931
+
932
+        // public API
933
+        this.start = start; //function() { start; }
934
+        this.stop  = stop; //function() { stop; }
935
+    };
936
+
937
+});
Browse code

removed CHANGELOG.txt README.md appinfo/info.xml appinfo/signature.json phone/scripts/app.js templates/settings.php css/style.css

DoubleBastionAdmin authored on 22/09/2022 08:32:29
Showing 1 changed files
1 1
deleted file mode 100644
... ...
@@ -1,923 +0,0 @@
1
-/**
2
- * @copyright 2021 Double Bastion LLC <www.doublebastion.com>
3
- *
4
- * @author Double Bastion LLC
5
- *
6
- * @license GNU AGPL version 3 or any later version
7
- *
8
- * This program is free software; you can redistribute it and/or
9
- * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
10
- * License as published by the Free Software Foundation; either
11
- * version 3 of the License, or any later version.
12
- *
13
- * This program is distributed in the hope that it will be useful,
14
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
- * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
17
- *
18
- * You should have received a copy of the GNU Affero General Public
19
- * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
20
- *
21
- *
22
- *
23
- * This is a modified version of the original file "app.js".
24
- *
25
- * We list below the copyright notice of the ctxSip phone (https://github.com/collecttix/ctxSip)
26
- * which also applies to the original "app.js" file, which was part of it:
27
- *
28
- *
29
- *  The MIT License (MIT)
30
- *
31
- *  Copyright (c) 2014 Collecttix
32
- *
33
- *  Permission is hereby granted, free of charge, to any person obtaining a copy
34
- *  of this software and associated documentation files (the "Software"), to deal
35
- *  in the Software without restriction, including without limitation the rights
36
- *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
37
- *  copies of the Software, and to permit persons to whom the Software is
38
- *  furnished to do so, subject to the following conditions:
39
- *
40
- *  The above copyright notice and this permission notice shall be included in
41
- *  all copies or substantial portions of the Software.
42
- *
43
- *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
44
- *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
45
- *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
46
- *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
47
- *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
48
- *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
49
- *  THE SOFTWARE.
50
- *
51
- */
52
-
53
-
54
-/* globals SIP, user, moment, Stopwatch */
55
-
56
-$(document).ready(function() {
57
-
58
-    var ctxSip;
59
-
60
-    // Show system notifications on incoming calls
61
-    function incomingCallNote() {
62
-       var noticeOptions = { body: "New incoming call !!!", icon: "images/sip_trip_phone_logo.svg" }
63
-       var inComingCallNotification = new Notification("SIP Trip Phone incoming call", noticeOptions);
64
-       inComingCallNotification.onclick = function (event) {
65
-         return;
66
-       }
67
-
68
-       if (document.hasFocus()) {
69
-           return;
70
-       } else { setTimeout(incomingCallNote, 8000); }
71
-    }
72
-
73
-    // Change page title on incoming calls
74
-    function changePageTitle() {
75
-        if ($(document).attr("title") == "SIP Trip Phone") { $(document).prop("title", "New call !!!"); } else { $(document).prop("title", "SIP Trip Phone"); }
76
-        if (document.hasFocus()) {
77
-            $(document).prop("title", "SIP Trip Phone");
78
-            return;
79
-        } else { setTimeout(changePageTitle, 460); }
80
-    }
81
-
82
-    var userSIPPass = window.opener.sipUserPasswd;
83
-
84
-    var user = JSON.parse(localStorage.getItem('SIPCreds'));
85
-
86
-    ctxSip = {
87
-
88
-        config : {
89
-            password        : userSIPPass,
90
-            displayName     : user.Display,
91
-            uri             : 'sip:'+user.User+'@'+user.Realm,
92
-            wsServers       : user.WSServer,
93
-            registerExpires : 9999999,
94
-            traceSip        : true,
95
-            stunServers: ["stun:" + user.Stun],
96
-            log             : {
97
-                level : 0,
98
-            }
99
-
100
-        },
101
-        ringtone     : document.getElementById('ringtone'),
102
-        ringbacktone : document.getElementById('ringbacktone'),
103
-        dtmfTone     : document.getElementById('dtmfTone'),
104
-
105
-        Sessions     : [],
106
-        callTimers   : {},
107
-        callActiveID : null,
108
-        callVolume   : 1,
109
-        Stream       : null,
110
-
111
-        /**
112
-         * Parses a SIP uri and returns a formatted phone number.
113
-         *
114
-         * @param  {string} phone number or uri to format
115
-         * @return {string}       formatted number
116
-         */
117
-        formatPhone : function(phone) {
118
-
119
-            var num;
120
-
121
-            if (phone.indexOf('@')) {
122
-                num =  phone.split('@')[0];
123
-            } else {
124
-                num = phone;
125
-            }
126
-
127
-            num = num.toString().replace(/[^0-9]/g, '');
128
-
129
-            if (num.length === 10) {
130
-                return '(' + num.substr(0, 3) + ') ' + num.substr(3, 3) + '-' + num.substr(6,4);
131
-            } else if (num.length === 11) {
132
-                return '(' + num.substr(1, 3) + ') ' + num.substr(4, 3) + '-' + num.substr(7,4);
133
-            } else {
134
-                return num;
135
-            }
136
-        },
137
-
138
-        // Sound methods
139
-        startRingTone : function() {
140
-            try { ctxSip.ringtone.play(); } catch (e) { }
141
-        },
142
-
143
-        stopRingTone : function() {
144
-            try { ctxSip.ringtone.pause(); } catch (e) { }
145
-        },
146
-
147
-        startRingbackTone : function() {
148
-            try { ctxSip.ringbacktone.play(); } catch (e) { }
149
-        },
150
-
151
-        stopRingbackTone : function() {
152
-            try { ctxSip.ringbacktone.pause(); } catch (e) { }
153
-        },
154
-
155
-        // Genereates a rendom string to ID a call
156
-        getUniqueID : function() {
157
-            return Math.random().toString(36).substr(2, 9);
158
-        },
159
-
160
-
161
-        newSession : function(newSess) {
162
-
163
-            newSess.displayName = newSess.remoteIdentity.displayName || newSess.remoteIdentity.uri.user;
164
-            newSess.ctxid       = ctxSip.getUniqueID();
165
-
166
-            var status;
167
-
168
-            if (newSess.direction === 'incoming') {
169
-                status = "Incoming: "+ newSess.displayName;
170
-                ctxSip.startRingTone();
171
-
172
-                incomingCallNote();
173
-                changePageTitle();
174
-
175
-            } else {
176
-                status = "Trying: "+ newSess.displayName;
177
-                ctxSip.startRingbackTone();
178
-            }
179
-
180
-            ctxSip.logCall(newSess, 'ringing');
181
-
182
-            ctxSip.setCallSessionStatus(status);
183
-
184
-            // EVENT CALLBACKS
185
-
186
-            newSess.on('progress',function(e) {
187
-                if (e.direction === 'outgoing') {
188
-                    ctxSip.setCallSessionStatus('Calling...');
189
-                }
190
-            });
191
-
192
-            newSess.on('connecting',function(e) {
193
-                if (e.direction === 'outgoing') {
194
-                    ctxSip.setCallSessionStatus('Connecting...');
195
-                }
196
-            });
197
-
198
-
199
-           newSess.on('accepted',function(e) {
200
-
201
-             // If there is another active call, hold it
202
-             if (ctxSip.callActiveID && ctxSip.callActiveID !== newSess.ctxid) {
203
-                 ctxSip.phoneHoldButtonPressed(ctxSip.callActiveID);
204
-             }
205
-
206
-             ctxSip.stopRingbackTone();
207
-             ctxSip.stopRingTone();
208
-             ctxSip.setCallSessionStatus('Answered');
209
-             ctxSip.logCall(newSess, 'answered');
210
-             ctxSip.callActiveID = newSess.ctxid;
211
-           });
212
-
213
-            newSess.on('hold', function(e) {
214
-                ctxSip.callActiveID = null;
215
-                ctxSip.logCall(newSess, 'holding');
216
-            });
217
-
218
-            newSess.on('unhold', function(e) {
219
-                ctxSip.logCall(newSess, 'resumed');
220
-                ctxSip.callActiveID = newSess.ctxid;
221
-            });
222
-
223
-            newSess.on('muted', function(e) {
224
-                ctxSip.Sessions[newSess.ctxid].isMuted = true;
225
-                ctxSip.setCallSessionStatus("Muted");
226
-            });
227
-
228
-            newSess.on('unmuted', function(e) {
229
-                ctxSip.Sessions[newSess.ctxid].isMuted = false;
230
-                ctxSip.setCallSessionStatus("Answered");
231
-            });
232
-
233
-            newSess.on('cancel', function(e) {
234
-                ctxSip.stopRingTone();
235
-                ctxSip.stopRingbackTone();
236
-                ctxSip.setCallSessionStatus("Canceled");
237
-                if (this.direction === 'outgoing') {
238
-                    ctxSip.callActiveID = null;
239
-                    newSess             = null;
240
-                    ctxSip.logCall(this, 'ended');
241
-                }
242
-            });
243
-
244
-            newSess.on('bye', function(e) {
245
-                ctxSip.stopRingTone();
246
-                ctxSip.stopRingbackTone();
247
-                ctxSip.setCallSessionStatus("");
248
-                ctxSip.logCall(newSess, 'ended');
249
-                ctxSip.callActiveID = null;
250
-                newSess             = null;
251
-            });
252
-
253
-            newSess.on('failed',function(e) {
254
-                ctxSip.stopRingTone();
255
-                ctxSip.stopRingbackTone();
256
-                ctxSip.setCallSessionStatus('Terminated');
257
-            });
258
-
259
-            newSess.on('rejected',function(e) {
260
-                ctxSip.stopRingTone();
261
-                ctxSip.stopRingbackTone();
262
-                ctxSip.setCallSessionStatus('Rejected');
263
-                ctxSip.callActiveID = null;
264
-                ctxSip.logCall(this, 'ended');
265
-                newSess             = null;
266
-            });
267
-
268
-            ctxSip.Sessions[newSess.ctxid] = newSess;
269
-
270
-        },
271
-
272
-        // getUser media request refused or device was not present
273
-        getUserMediaFailure : function(e) {
274
-            window.console.error('getUserMedia failed:', e);
275
-            ctxSip.setError(true, 'Media Error.', 'You must allow access to your microphone.  Check the address bar.', true);
276
-        },
277
-
278
-
279
-        getUserMediaSuccess : function(stream) {
280
-            ctxSip.Stream = stream;
281
-        },
282
-
283
-
284
-        /**
285
-         * sets the ui call status field
286
-         *
287
-         * @param {string} status
288
-         */
289
-        setCallSessionStatus : function(status) {
290
-            $('#txtCallStatus').html(status);
291
-        },
292
-
293
-        /**
294
-         * sets the ui connection status field
295
-         *
296
-         * @param {string} status
297
-         */
298
-        setStatus : function(status) {
299
-            $("#txtRegStatus").html('<i class="fa fa-signal"></i> '+status);
300
-        },
301
-
302
-        /**
303
-         * logs a call to localstorage
304
-         *
305
-         * @param  {object} session
306
-         * @param  {string} status Enum 'ringing', 'answered', 'ended', 'holding', 'resumed'
307
-         */
308
-        logCall : function(session, status) {
309
-
310
-            var log = {
311
-                    clid : session.displayName,
312
-                    uri  : session.remoteIdentity.uri.toString(),
313
-                    id   : session.ctxid,
314
-                    time : new Date().getTime()
315
-                },
316
-                calllog = JSON.parse(localStorage.getItem('sipCalls'));
317
-
318
-            if (!calllog) { calllog = {}; }
319
-
320
-            if (!calllog.hasOwnProperty(session.ctxid)) {
321
-                calllog[log.id] = {
322
-                    id    : log.id,
323
-                    clid  : log.clid,
324
-                    uri   : log.uri,
325
-                    start : log.time,
326
-                    flow  : session.direction
327
-                };
328
-            }
329
-
330
-            if (status === 'ended') {
331
-                calllog[log.id].stop = log.time;
332
-            }
333
-
334
-            if (status === 'ended' && calllog[log.id].status === 'ringing') {
335
-                calllog[log.id].status = 'missed';
336
-            } else {
337
-                calllog[log.id].status = status;
338
-            }
339
-
340
-            localStorage.setItem('sipCalls', JSON.stringify(calllog));
341
-            ctxSip.logShow();
342
-        },
343
-
344
-        /**
345
-         * adds a ui item to the call log
346
-         *
347
-         * @param  {object} item log item
348
-         */
349
-        logItem : function(item) {
350
-
351
-            var callActive = (item.status !== 'ended' && item.status !== 'missed'),
352
-                callLength = (item.status !== 'ended')? '<span id="'+item.id+'"></span>': moment.duration(item.stop - item.start).humanize(),
353
-                callClass  = '',
354
-                callIcon,
355
-                i;
356
-
357
-            switch (item.status) {
358
-                case 'ringing'  :
359
-                    callClass = 'list-group-item-success';
360
-                    callIcon  = 'fa-bell';
361
-                    break;
362
-
363
-                case 'missed'   :
364
-                    callClass = 'list-group-item-danger';
365
-                    if (item.flow === "incoming") { callIcon = 'fa-chevron-left'; }
366
-                    if (item.flow === "outgoing") { callIcon = 'fa-chevron-right'; }
367
-                    break;
368
-
369
-                case 'holding'  :
370
-                    callClass = 'list-group-item-warning';
371
-                    callIcon  = 'fa-pause';
372
-                    break;
373
-
374
-                case 'answered' :
375
-                case 'resumed'  :
376
-                    callClass = 'list-group-item-info';
377
-                    callIcon  = 'fa-phone-square';
378
-                    break;
379
-
380
-                case 'ended'  :
381
-                    if (item.flow === "incoming") { callIcon = 'fa-chevron-left'; }
382
-                    if (item.flow === "outgoing") { callIcon = 'fa-chevron-right'; }
383
-                    break;
384
-            }
385
-
386
-
387
-            i  = '<div class="list-group-item sip-logitem clearfix '+callClass+'" data-uri="'+item.uri+'" data-sessionid="'+item.id+'" title="Double Click to Call">';
388
-            i += '<div class="clearfix"><div class="pull-left">';
389
-            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>';
390
-            i += '</div>';
391
-            i += '<div class="pull-right text-right"><em>'+item.clid+'</em><br>' + callLength+'</div></div>';
392
-
393
-            if (callActive) {
394
-                i += '<div class="btn-group btn-group-xs pull-right">';
395
-                if (item.status === 'ringing' && item.flow === 'incoming') {
396
-                    i += '<button class="btn btn-xs btn-success btnCall" title="Call"><i class="fa fa-phone"></i></button>';
397
-                } else {
398
-                    i += '<button class="btn btn-xs btn-primary btnHoldResume" title="Hold"><i class="fa fa-pause"></i></button>';
399
-                    i += '<button class="btn btn-xs btn-info btnTransfer" title="Transfer"><i class="fa fa-random"></i></button>';
400
-                    i += '<button class="btn btn-xs btn-warning btnMute" title="Mute"><i class="fa fa-fw fa-microphone"></i></button>';
401
-                }
402
-                i += '<button class="btn btn-xs btn-danger btnHangUp" title="Hangup"><i class="fa fa-stop"></i></button>';
403
-                i += '</div>';
404
-            }
405
-            i += '</div>';
406
-
407
-            $('#sip-logitems').append(i);
408
-
409
-
410
-            // Start call timer on answer
411
-            if (item.status === 'answered') {
412
-                var tEle = document.getElementById(item.id);
413
-                ctxSip.callTimers[item.id] = new Stopwatch(tEle);
414
-                ctxSip.callTimers[item.id].start();
415
-            }
416
-
417
-            if (callActive && item.status !== 'ringing') {
418
-                ctxSip.callTimers[item.id].start({startTime : item.start});
419
-            }
420
-
421
-            $('#sip-logitems').scrollTop(0);
422
-        },
423
-
424
-        /**
425
-         * updates the call log ui
426
-         */
427
-        logShow : function() {
428
-
429
-            var calllog = JSON.parse(localStorage.getItem('sipCalls')),
430
-            x = [];
431
-
432
-            if (calllog !== null) {
433
-
434
-                $('#sip-splash').addClass('hide');
435
-                $('#sip-log').removeClass('hide');
436
-
437
-                // empty existing logs
438
-                $('#sip-logitems').empty();
439
-
440
-                // JS doesn't guarantee property order so
441
-                // create an array with the start time as
442
-                // the key and sort by that.
443
-
444
-                // Add start time to array
445
-                $.each(calllog, function(k,v) {
446
-                    x.push(v);
447
-                });
448
-
449
-                // sort descending
450
-                x.sort(function(a, b) {
451
-                    return b.start - a.start;
452
-                });
453
-
454
-                $.each(x, function(k, v) {
455
-                    ctxSip.logItem(v);
456
-                });
457
-
458
-            } else {
459
-                $('#sip-splash').removeClass('hide');
460
-                $('#sip-log').addClass('hide');
461
-            }
462
-        },
463
-
464
-        /**
465
-         * removes log items from localstorage and updates the UI
466
-         */
467
-        logClear : function() {
468
-
469
-            localStorage.removeItem('sipCalls');
470
-            ctxSip.logShow();
471
-        },
472
-
473
-        sipCall : function(target) {
474
-
475
-            try {
476
-                var s = ctxSip.phone.invite(target, {
477
-                    media : {
478
-                        stream      : ctxSip.Stream,
479
-                        constraints : { audio : true, video : false },
480
-                        render      : { remote: document.getElementById('audioRemote') }
481
-                        //render: { remote: $('#audioRemote').get()[0] }
482
-                        //RTCConstraints : { "optional": [{ 'DtlsSrtpKeyAgreement': 'true'} ]}
483
-                    }
484
-                });
485
-                s.direction = 'outgoing';
486
-                ctxSip.newSession(s);
487
-
488
-            } catch(e) {
489
-                throw(e);
490
-            }
491
-        },
492
-
493
-        sipTransfer : function(sessionid) {
494
-
495
-                var s  = ctxSip.Sessions[sessionid],
496
-                target = window.prompt('Enter destination number', '');
497
-
498
-            ctxSip.setCallSessionStatus('<i>Transfering the call...</i>');
499
-            s.refer(target);
500
-        },
501
-
502
-        sipHangUp : function(sessionid) {
503
-
504
-            var s = ctxSip.Sessions[sessionid];
505
-            // s.terminate();
506
-            if (!s) {
507
-                return;
508
-            } else if (s.startTime) {
509
-                s.bye();
510
-            } else if (s.reject) {
511
-                s.reject();
512
-            } else if (s.cancel) {
513
-                s.cancel();
514
-            }
515
-
516
-        },
517
-
518
-        sipSendDTMF : function(digit) {
519
-
520
-            try { ctxSip.dtmfTone.play(); } catch(e) { }
521
-
522
-            var a = ctxSip.callActiveID;
523
-            if (a) {
524
-                var s = ctxSip.Sessions[a];
525
-                s.dtmf(digit);
526
-            }
527
-        },
528
-
529
-        phoneCallButtonPressed : function(sessionid) {
530
-
531
-                var s  = ctxSip.Sessions[sessionid],
532
-                target = $("#numDisplay").val();
533
-
534
-            if (!s) {
535
-
536
-                $("#numDisplay").val("");
537
-                ctxSip.sipCall(target);
538
-
539
-            } else if (s.accept && !s.startTime) {
540
-
541
-                s.accept({
542
-                    media: {
543
-                            stream: ctxSip.Stream,
544
-                            constraints: { audio: true, video: false },
545
-                            render      : { remote: document.getElementById('audioRemote') }
546
-                            //render: { remote: $('#audioRemote').get()[0] }
547
-                            // RTCConstraints : { "optional": [{ 'DtlsSrtpKeyAgreement': 'true'} ]}
548
-                           }
549
-                });
550
-            }
551
-        },
552
-
553
-        phoneMuteButtonPressed : function (sessionid) {
554
-
555
-            var s = ctxSip.Sessions[sessionid];
556
-
557
-            if (!s.isMuted) {
558
-                s.mute();
559
-            } else {
560
-                s.unmute();
561
-            }
562
-        },
563
-
564
-        phoneHoldButtonPressed : function(sessionid) {
565
-
566
-            var s = ctxSip.Sessions[sessionid];
567
-
568
-            if (s.isOnHold().local === true) {
569
-                s.unhold();
570
-            } else {
571
-                s.hold();
572
-            }
573
-        },
574
-
575
-
576
-        setError : function(err, title, msg, closable) {
577
-
578
-            // Show modal if err = true
579
-            if (err === true) {
580
-                $("#mdlError p").html(msg);
581
-                $("#mdlError").modal('show');
582
-
583
-                if (closable) {
584
-                    var b = '<button type="button" class="close" data-dismiss="modal">&times;</button>';
585
-                    $("#mdlError .modal-header").find('button').remove();
586
-                    $("#mdlError .modal-header").prepend(b);
587
-                    $("#mdlError .modal-title").html(title);
588
-                    $("#mdlError").modal({ keyboard : true });
589
-                } else {
590
-                    $("#mdlError .modal-header").find('button').remove();
591
-                    $("#mdlError .modal-title").html(title);
592
-                    $("#mdlError").modal({ keyboard : false });
593
-                }
594
-                $('#numDisplay').prop('disabled', 'disabled');
595
-            } else {
596
-                $('#numDisplay').removeProp('disabled');
597
-                $("#mdlError").modal('hide');
598
-            }
599
-        },
600
-
601
-        /**
602
-         * Tests for a capable browser, return bool, and shows an
603
-         * error modal on fail.
604
-         */
605
-        hasWebRTC : function() {
606
-
607
-            if (navigator.webkitGetUserMedia) {
608
-                return true;
609
-            } else if (navigator.mozGetUserMedia) {
610
-                return true;
611
-            } else if (navigator.getUserMedia) {
612
-                return true;
613
-            } else {
614
-                ctxSip.setError(true, 'Unsupported Browser.', 'Your browser does not support the features required for this phone.');
615
-                window.console.error("WebRTC support not found");
616
-                return false;
617
-            }
618
-        }
619
-    };
620
-
621
-    userSIPPass = '';
622
-    window.opener.sipUserPasswd = '';
623
-
624
-    // Throw an error if the browser can't hack it.
625
-    if (!ctxSip.hasWebRTC()) {
626
-        return true;
627
-    }
628
-
629
-    ctxSip.phone = new SIP.UA(ctxSip.config);
630
-
631
-    ctxSip.phone.on('connected', function(e) {
632
-        ctxSip.setStatus("Connected");
633
-    });
634
-
635
-    ctxSip.phone.on('disconnected', function(e) {
636
-        ctxSip.setStatus("Disconnected");
637
-
638
-        // disable phone
639
-        ctxSip.setError(true, 'Websocket Disconnected.', 'An Error occurred connecting to the websocket.');
640
-
641
-        // remove existing sessions
642
-        $("#sessions > .session").each(function(i, session) {
643
-            ctxSip.removeSession(session, 500);
644
-        });
645
-    });
646
-
647
-    ctxSip.phone.on('registered', function(e) {
648
-
649
-        var closeEditorWarning = function() {
650
-            return 'If you close this window, you will not be able to make or receive calls from your browser.';
651
-        };
652
-
653
-        var closePhone = function() {
654
-            // stop the phone on unload
655
-//            localStorage.removeItem('ctxPhone');
656
-            localStorage.removeItem('SipTripPhone');
657
-            ctxSip.phone.stop();
658
-        };
659
-
660
-        window.onbeforeunload = closeEditorWarning;
661
-        window.onunload       = closePhone;
662
-
663
-        // This key is set to prevent multiple windows.
664
-        localStorage.setItem('SipTripPhone', 'true');
665
-
666
-        $("#mldError").modal('hide');
667
-        ctxSip.setStatus("Ready");
668
-
669
-        // Get the userMedia and cache the stream
670
-        var audio = document.getElementById('audioRemote');
671
-        var mediaStream = new MediaStream();
672
-        let audioTrack = null;
673
-
674
-        navigator.mediaDevices.getUserMedia({ audio : true, video : false }, ctxSip.getUserMediaSuccess, ctxSip.getUserMediaFailure).then(function(mediaStream) {
675
-
676
-           let audioTracks = mediaStream.getAudioTracks();
677
-           audio.srcObject = mediaStream;
678
-
679
-           if (audioTracks.length) {
680
-               audioTrack = audioTracks[0];
681
-           }
682
-        }).then(function() {
683
-           new Promise(function(resolve) {
684
-               audio.onloadedmetadata = resolve;
685
-           })
686
-        })
687
-
688
-    });
689
-
690
-    ctxSip.phone.on('registrationFailed', function(e) {
691
-        ctxSip.setError(true, 'Registration Error.', 'An Error occurred registering your phone. Check your settings.');
692
-        ctxSip.setStatus("Error: Registration Failed");
693
-    });
694
-
695
-    ctxSip.phone.on('unregistered', function(e) {
696
-        ctxSip.setError(true, 'Registration Error.', 'An Error occurred registering your phone. Check your settings.');
697
-        ctxSip.setStatus("Error: Registration Failed");
698
-    });
699
-
700
-    ctxSip.phone.on('invite', function (incomingSession) {
701
-
702
-        var s = incomingSession;
703
-
704
-        s.direction = 'incoming';
705
-        ctxSip.newSession(s);
706
-    });
707
-
708
-    // Auto-focus number input on backspace.
709
-    $('#sipClient').keydown(function(event) {
710
-        if (event.which === 8) {
711
-            $('#numDisplay').focus();
712
-        }
713
-    });
714
-
715
-    $('#numDisplay').keypress(function(e) {
716
-        // Enter pressed? so Dial.
717
-        if (e.which === 13) {
718
-            ctxSip.phoneCallButtonPressed();
719
-        }
720
-    });
721
-
722
-    var clck = 0;
723
-
724
-    $('.digit').click(function(event) {
725
-
726
-     if (event.shiftKey) {
727
-
728
-         clck++;
729
-         event.preventDefault();
730
-         var num = $('#numDisplay').val();
731
-         var dig;
732
-         var diginit = $(this).data('digit').toString().split(',');
733
-         var elct = diginit.length;
734
-
735
-         dig = diginit[clck%elct];
736
-         var numsec = num.slice(0,-1);
737
-         $('#numDisplay').val(numsec+dig);
738
-         ctxSip.sipSendDTMF(dig);
739
-
740
-     } else {
741
-         event.preventDefault();
742
-         var num = $('#numDisplay').val();
743
-         var dig;
744
-         var diginit = $(this).data('digit').toString().split(',');
745
-
746
-         dig = diginit[0];
747
-         clck = 0;
748
-         $('#numDisplay').val(num+dig);
749
-         ctxSip.sipSendDTMF(dig);
750
-       }
751
-
752
-       return false;
753
-
754
-    });
755
-
756
-    $('#phoneUI .dropdown-menu').click(function(e) {
757
-        e.preventDefault();
758
-    });
759
-
760
-    $('#phoneUI').delegate('.btnCall', 'click', function(event) {
761
-        ctxSip.phoneCallButtonPressed();
762
-        // to close the dropdown
763
-        return true;
764
-    });
765
-
766
-    $('.sipLogClear').click(function(event) {
767
-        event.preventDefault();
768
-        ctxSip.logClear();
769
-    });
770
-
771
-    $('#sip-logitems').delegate('.sip-logitem .btnCall', 'click', function(event) {
772
-        var sessionid = $(this).closest('.sip-logitem').data('sessionid');
773
-        ctxSip.phoneCallButtonPressed(sessionid);
774
-        return false;
775
-    });
776
-
777
-    $('#sip-logitems').delegate('.sip-logitem .btnHoldResume', 'click', function(event) {
778
-        var sessionid = $(this).closest('.sip-logitem').data('sessionid');
779
-        ctxSip.phoneHoldButtonPressed(sessionid);
780
-        return false;
781
-    });
782
-
783
-    $('#sip-logitems').delegate('.sip-logitem .btnHangUp', 'click', function(event) {
784
-        var sessionid = $(this).closest('.sip-logitem').data('sessionid');
785
-        ctxSip.sipHangUp(sessionid);
786
-        return false;
787
-    });
788
-
789
-    $('#sip-logitems').delegate('.sip-logitem .btnTransfer', 'click', function(event) {
790
-        var sessionid = $(this).closest('.sip-logitem').data('sessionid');
791
-        ctxSip.sipTransfer(sessionid);
792
-        return false;
793
-    });
794
-
795
-    $('#sip-logitems').delegate('.sip-logitem .btnMute', 'click', function(event) {
796
-        var sessionid = $(this).closest('.sip-logitem').data('sessionid');
797
-        ctxSip.phoneMuteButtonPressed(sessionid);
798
-        return false;
799
-    });
800
-
801
-    $('#sip-logitems').delegate('.sip-logitem', 'dblclick', function(event) {
802
-        event.preventDefault();
803
-
804
-        var uri = $(this).data('uri');
805
-        $('#numDisplay').val(uri);
806
-        ctxSip.phoneCallButtonPressed();
807
-    });
808
-
809
-    $('#sldVolume').on('change', function() {
810
-
811
-            var v  = $(this).val() / 100,
812
-            btn    = $('#btnVol'),
813
-            icon   = $('#btnVol').find('i'),
814
-            active = ctxSip.callActiveID;
815
-
816
-        // Set the object and media stream volumes
817
-        if (ctxSip.Sessions[active]) {
818
-            ctxSip.Sessions[active].player.volume = v;
819
-            ctxSip.callVolume                     = v;
820
-        }
821
-
822
-        // Set the others
823
-        $('audio').each(function() {
824
-            $(this).get()[0].volume = v;
825
-        });
826
-
827
-        if (v < 0.1) {
828
-            btn.removeClass(function (index, css) {
829
-                   return (css.match (/(^|\s)btn\S+/g) || []).join(' ');
830
-                })
831
-                .addClass('btn btn-sm btn-danger');
832
-            icon.removeClass().addClass('fa fa-fw fa-volume-off');
833
-        } else if (v < 0.8) {
834
-            btn.removeClass(function (index, css) {
835
-                   return (css.match (/(^|\s)btn\S+/g) || []).join(' ');
836
-               }).addClass('btn btn-sm btn-info');
837
-            icon.removeClass().addClass('fa fa-fw fa-volume-down');
838
-        } else {
839
-            btn.removeClass(function (index, css) {
840
-                   return (css.match (/(^|\s)btn\S+/g) || []).join(' ');
841
-               }).addClass('btn btn-sm btn-primary');
842
-            icon.removeClass().addClass('fa fa-fw fa-volume-up');
843
-        }
844
-        return false;
845
-    });
846
-
847
-    // Hide the spalsh after 3 secs.
848
-    setTimeout(function() {
849
-        ctxSip.logShow();
850
-    }, 3000);
851
-
852
-
853
-    /**
854
-     * Stopwatch object used for call timers
855
-     *
856
-     * @param {dom element} elem
857
-     * @param {[object]} options
858
-     */
859
-    var Stopwatch = function(elem, options) {
860
-
861
-        // private functions
862
-        function createTimer() {
863
-            return document.createElement("span");
864
-        }
865
-
866
-        var timer = createTimer(),
867
-            offset,
868
-            clock,
869
-            interval;
870
-
871
-        // default options
872
-        options           = options || {};
873
-        options.delay     = options.delay || 1000;
874
-        options.startTime = options.startTime || Date.now();
875
-
876
-        // append elements
877
-        elem.appendChild(timer);
878
-
879
-        function start() {
880
-            if (!interval) {
881
-                offset   = options.startTime;
882
-                interval = setInterval(update, options.delay);
883
-            }
884
-        }
885
-
886
-        function stop() {
887
-            if (interval) {
888
-                clearInterval(interval);
889
-                interval = null;
890
-            }
891
-        }
892
-
893
-        function reset() {
894
-            clock = 0;
895
-            render();
896
-        }
897
-
898
-        function update() {
899
-            clock += delta();
900
-            render();
901
-        }
902
-
903
-        function render() {
904
-            timer.innerHTML = moment(clock).format('mm:ss');
905
-        }
906
-
907
-        function delta() {
908
-            var now = Date.now(),
909
-                d   = now - offset;
910
-
911
-            offset = now;
912
-            return d;
913
-        }
914
-
915
-        // initialize
916
-        reset();
917
-
918
-        // public API
919
-        this.start = start; //function() { start; }
920
-        this.stop  = stop; //function() { stop; }
921
-    };
922
-
923
-});
Browse code

Created repository.

DoubleBastionAdmin authored on 02/03/2022 00:26:46
Showing 1 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,923 @@
1
+/**
2
+ * @copyright 2021 Double Bastion LLC <www.doublebastion.com>
3
+ *
4
+ * @author Double Bastion LLC
5
+ *
6
+ * @license GNU AGPL version 3 or any later version
7
+ *
8
+ * This program is free software; you can redistribute it and/or
9
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
10
+ * License as published by the Free Software Foundation; either
11
+ * version 3 of the License, or any later version.
12
+ *
13
+ * This program is distributed in the hope that it will be useful,
14
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
17
+ *
18
+ * You should have received a copy of the GNU Affero General Public
19
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
20
+ *
21
+ *
22
+ *
23
+ * This is a modified version of the original file "app.js".
24
+ *
25
+ * We list below the copyright notice of the ctxSip phone (https://github.com/collecttix/ctxSip)
26
+ * which also applies to the original "app.js" file, which was part of it:
27
+ *
28
+ *
29
+ *  The MIT License (MIT)
30
+ *
31
+ *  Copyright (c) 2014 Collecttix
32
+ *
33
+ *  Permission is hereby granted, free of charge, to any person obtaining a copy
34
+ *  of this software and associated documentation files (the "Software"), to deal
35
+ *  in the Software without restriction, including without limitation the rights
36
+ *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
37
+ *  copies of the Software, and to permit persons to whom the Software is
38
+ *  furnished to do so, subject to the following conditions:
39
+ *
40
+ *  The above copyright notice and this permission notice shall be included in
41
+ *  all copies or substantial portions of the Software.
42
+ *
43
+ *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
44
+ *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
45
+ *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
46
+ *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
47
+ *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
48
+ *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
49
+ *  THE SOFTWARE.
50
+ *
51
+ */
52
+
53
+
54
+/* globals SIP, user, moment, Stopwatch */
55
+
56
+$(document).ready(function() {
57
+
58
+    var ctxSip;
59
+
60
+    // Show system notifications on incoming calls
61
+    function incomingCallNote() {
62
+       var noticeOptions = { body: "New incoming call !!!", icon: "images/sip_trip_phone_logo.svg" }
63
+       var inComingCallNotification = new Notification("SIP Trip Phone incoming call", noticeOptions);
64
+       inComingCallNotification.onclick = function (event) {
65
+         return;
66
+       }
67
+
68
+       if (document.hasFocus()) {
69
+           return;
70
+       } else { setTimeout(incomingCallNote, 8000); }
71
+    }
72
+
73
+    // Change page title on incoming calls
74
+    function changePageTitle() {
75
+        if ($(document).attr("title") == "SIP Trip Phone") { $(document).prop("title", "New call !!!"); } else { $(document).prop("title", "SIP Trip Phone"); }
76
+        if (document.hasFocus()) {
77
+            $(document).prop("title", "SIP Trip Phone");
78
+            return;
79
+        } else { setTimeout(changePageTitle, 460); }
80
+    }
81
+
82
+    var userSIPPass = window.opener.sipUserPasswd;
83
+
84
+    var user = JSON.parse(localStorage.getItem('SIPCreds'));
85
+
86
+    ctxSip = {
87
+
88
+        config : {
89
+            password        : userSIPPass,
90
+            displayName     : user.Display,
91
+            uri             : 'sip:'+user.User+'@'+user.Realm,
92
+            wsServers       : user.WSServer,
93
+            registerExpires : 9999999,
94
+            traceSip        : true,
95
+            stunServers: ["stun:" + user.Stun],
96
+            log             : {
97
+                level : 0,
98
+            }
99
+
100
+        },
101
+        ringtone     : document.getElementById('ringtone'),
102
+        ringbacktone : document.getElementById('ringbacktone'),
103
+        dtmfTone     : document.getElementById('dtmfTone'),
104
+
105
+        Sessions     : [],
106
+        callTimers   : {},
107
+        callActiveID : null,
108
+        callVolume   : 1,
109
+        Stream       : null,
110
+
111
+        /**
112
+         * Parses a SIP uri and returns a formatted phone number.
113
+         *
114
+         * @param  {string} phone number or uri to format
115
+         * @return {string}       formatted number
116
+         */
117
+        formatPhone : function(phone) {
118
+
119
+            var num;
120
+
121
+            if (phone.indexOf('@')) {
122
+                num =  phone.split('@')[0];
123
+            } else {
124
+                num = phone;
125
+            }
126
+
127
+            num = num.toString().replace(/[^0-9]/g, '');
128
+
129
+            if (num.length === 10) {
130
+                return '(' + num.substr(0, 3) + ') ' + num.substr(3, 3) + '-' + num.substr(6,4);
131
+            } else if (num.length === 11) {
132
+                return '(' + num.substr(1, 3) + ') ' + num.substr(4, 3) + '-' + num.substr(7,4);
133
+            } else {
134
+                return num;
135
+            }
136
+        },
137
+
138
+        // Sound methods
139
+        startRingTone : function() {
140
+            try { ctxSip.ringtone.play(); } catch (e) { }
141
+        },
142
+
143
+        stopRingTone : function() {
144
+            try { ctxSip.ringtone.pause(); } catch (e) { }
145
+        },
146
+
147
+        startRingbackTone : function() {
148
+            try { ctxSip.ringbacktone.play(); } catch (e) { }
149
+        },
150
+
151
+        stopRingbackTone : function() {
152
+            try { ctxSip.ringbacktone.pause(); } catch (e) { }
153
+        },
154
+
155
+        // Genereates a rendom string to ID a call
156
+        getUniqueID : function() {
157
+            return Math.random().toString(36).substr(2, 9);
158
+        },
159
+
160
+
161
+        newSession : function(newSess) {
162
+
163
+            newSess.displayName = newSess.remoteIdentity.displayName || newSess.remoteIdentity.uri.user;
164
+            newSess.ctxid       = ctxSip.getUniqueID();
165
+
166
+            var status;
167
+
168
+            if (newSess.direction === 'incoming') {
169
+                status = "Incoming: "+ newSess.displayName;
170
+                ctxSip.startRingTone();
171
+
172
+                incomingCallNote();
173
+                changePageTitle();
174
+
175
+            } else {
176
+                status = "Trying: "+ newSess.displayName;
177
+                ctxSip.startRingbackTone();
178
+            }
179
+
180
+            ctxSip.logCall(newSess, 'ringing');
181
+
182
+            ctxSip.setCallSessionStatus(status);
183
+
184
+            // EVENT CALLBACKS
185
+
186
+            newSess.on('progress',function(e) {
187
+                if (e.direction === 'outgoing') {
188
+                    ctxSip.setCallSessionStatus('Calling...');
189
+                }
190
+            });
191
+
192
+            newSess.on('connecting',function(e) {
193
+                if (e.direction === 'outgoing') {
194
+                    ctxSip.setCallSessionStatus('Connecting...');
195
+                }
196
+            });
197
+
198
+
199
+           newSess.on('accepted',function(e) {
200
+
201
+             // If there is another active call, hold it
202
+             if (ctxSip.callActiveID && ctxSip.callActiveID !== newSess.ctxid) {
203
+                 ctxSip.phoneHoldButtonPressed(ctxSip.callActiveID);
204
+             }
205
+
206
+             ctxSip.stopRingbackTone();
207
+             ctxSip.stopRingTone();
208
+             ctxSip.setCallSessionStatus('Answered');
209
+             ctxSip.logCall(newSess, 'answered');
210
+             ctxSip.callActiveID = newSess.ctxid;
211
+           });
212
+
213
+            newSess.on('hold', function(e) {
214
+                ctxSip.callActiveID = null;
215
+                ctxSip.logCall(newSess, 'holding');
216
+            });
217
+
218
+            newSess.on('unhold', function(e) {
219
+                ctxSip.logCall(newSess, 'resumed');
220
+                ctxSip.callActiveID = newSess.ctxid;
221
+            });
222
+
223
+            newSess.on('muted', function(e) {
224
+                ctxSip.Sessions[newSess.ctxid].isMuted = true;
225
+                ctxSip.setCallSessionStatus("Muted");
226
+            });
227
+
228
+            newSess.on('unmuted', function(e) {
229
+                ctxSip.Sessions[newSess.ctxid].isMuted = false;
230
+                ctxSip.setCallSessionStatus("Answered");
231
+            });
232
+
233
+            newSess.on('cancel', function(e) {
234
+                ctxSip.stopRingTone();
235
+                ctxSip.stopRingbackTone();
236
+                ctxSip.setCallSessionStatus("Canceled");
237
+                if (this.direction === 'outgoing') {
238
+                    ctxSip.callActiveID = null;
239
+                    newSess             = null;
240
+                    ctxSip.logCall(this, 'ended');
241
+                }
242
+            });
243
+
244
+            newSess.on('bye', function(e) {
245
+                ctxSip.stopRingTone();
246
+                ctxSip.stopRingbackTone();
247
+                ctxSip.setCallSessionStatus("");
248
+                ctxSip.logCall(newSess, 'ended');
249
+                ctxSip.callActiveID = null;
250
+                newSess             = null;
251
+            });
252
+
253
+            newSess.on('failed',function(e) {
254
+                ctxSip.stopRingTone();
255
+                ctxSip.stopRingbackTone();
256
+                ctxSip.setCallSessionStatus('Terminated');
257
+            });
258
+
259
+            newSess.on('rejected',function(e) {
260
+                ctxSip.stopRingTone();
261
+                ctxSip.stopRingbackTone();
262
+                ctxSip.setCallSessionStatus('Rejected');
263
+                ctxSip.callActiveID = null;
264
+                ctxSip.logCall(this, 'ended');
265
+                newSess             = null;
266
+            });
267
+
268
+            ctxSip.Sessions[newSess.ctxid] = newSess;
269
+
270
+        },
271
+
272
+        // getUser media request refused or device was not present
273
+        getUserMediaFailure : function(e) {
274
+            window.console.error('getUserMedia failed:', e);
275
+            ctxSip.setError(true, 'Media Error.', 'You must allow access to your microphone.  Check the address bar.', true);
276
+        },
277
+
278
+
279
+        getUserMediaSuccess : function(stream) {
280
+            ctxSip.Stream = stream;
281
+        },
282
+
283
+
284
+        /**
285
+         * sets the ui call status field
286
+         *
287
+         * @param {string} status
288
+         */
289
+        setCallSessionStatus : function(status) {
290
+            $('#txtCallStatus').html(status);
291
+        },
292
+
293
+        /**
294
+         * sets the ui connection status field
295
+         *
296
+         * @param {string} status
297
+         */
298
+        setStatus : function(status) {
299
+            $("#txtRegStatus").html('<i class="fa fa-signal"></i> '+status);
300
+        },
301
+
302
+        /**
303
+         * logs a call to localstorage
304
+         *
305
+         * @param  {object} session
306
+         * @param  {string} status Enum 'ringing', 'answered', 'ended', 'holding', 'resumed'
307
+         */
308
+        logCall : function(session, status) {
309
+
310
+            var log = {
311
+                    clid : session.displayName,
312
+                    uri  : session.remoteIdentity.uri.toString(),
313
+                    id   : session.ctxid,
314
+                    time : new Date().getTime()
315
+                },
316
+                calllog = JSON.parse(localStorage.getItem('sipCalls'));
317
+
318
+            if (!calllog) { calllog = {}; }
319
+
320
+            if (!calllog.hasOwnProperty(session.ctxid)) {
321
+                calllog[log.id] = {
322
+                    id    : log.id,
323
+                    clid  : log.clid,
324
+                    uri   : log.uri,
325
+                    start : log.time,
326
+                    flow  : session.direction
327
+                };
328
+            }
329
+
330
+            if (status === 'ended') {
331
+                calllog[log.id].stop = log.time;
332
+            }
333
+
334
+            if (status === 'ended' && calllog[log.id].status === 'ringing') {
335
+                calllog[log.id].status = 'missed';
336
+            } else {
337
+                calllog[log.id].status = status;
338
+            }
339
+
340
+            localStorage.setItem('sipCalls', JSON.stringify(calllog));
341
+            ctxSip.logShow();
342
+        },
343
+
344
+        /**
345
+         * adds a ui item to the call log
346
+         *
347
+         * @param  {object} item log item
348
+         */
349
+        logItem : function(item) {
350
+
351
+            var callActive = (item.status !== 'ended' && item.status !== 'missed'),
352
+                callLength = (item.status !== 'ended')? '<span id="'+item.id+'"></span>': moment.duration(item.stop - item.start).humanize(),
353
+                callClass  = '',
354
+                callIcon,
355
+                i;
356
+
357
+            switch (item.status) {
358
+                case 'ringing'  :
359
+                    callClass = 'list-group-item-success';
360
+                    callIcon  = 'fa-bell';
361
+                    break;
362
+
363
+                case 'missed'   :
364
+                    callClass = 'list-group-item-danger';
365
+                    if (item.flow === "incoming") { callIcon = 'fa-chevron-left'; }
366
+                    if (item.flow === "outgoing") { callIcon = 'fa-chevron-right'; }
367
+                    break;
368
+
369
+                case 'holding'  :
370
+                    callClass = 'list-group-item-warning';
371
+                    callIcon  = 'fa-pause';
372
+                    break;
373
+
374
+                case 'answered' :
375
+                case 'resumed'  :
376
+                    callClass = 'list-group-item-info';
377
+                    callIcon  = 'fa-phone-square';
378
+                    break;
379
+
380
+                case 'ended'  :
381
+                    if (item.flow === "incoming") { callIcon = 'fa-chevron-left'; }
382
+                    if (item.flow === "outgoing") { callIcon = 'fa-chevron-right'; }
383
+                    break;
384
+            }
385
+
386
+
387
+            i  = '<div class="list-group-item sip-logitem clearfix '+callClass+'" data-uri="'+item.uri+'" data-sessionid="'+item.id+'" title="Double Click to Call">';
388
+            i += '<div class="clearfix"><div class="pull-left">';
389
+            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>';
390
+            i += '</div>';
391
+            i += '<div class="pull-right text-right"><em>'+item.clid+'</em><br>' + callLength+'</div></div>';
392
+
393
+            if (callActive) {
394
+                i += '<div class="btn-group btn-group-xs pull-right">';
395
+                if (item.status === 'ringing' && item.flow === 'incoming') {
396
+                    i += '<button class="btn btn-xs btn-success btnCall" title="Call"><i class="fa fa-phone"></i></button>';
397
+                } else {
398
+                    i += '<button class="btn btn-xs btn-primary btnHoldResume" title="Hold"><i class="fa fa-pause"></i></button>';
399
+                    i += '<button class="btn btn-xs btn-info btnTransfer" title="Transfer"><i class="fa fa-random"></i></button>';
400
+                    i += '<button class="btn btn-xs btn-warning btnMute" title="Mute"><i class="fa fa-fw fa-microphone"></i></button>';
401
+                }
402
+                i += '<button class="btn btn-xs btn-danger btnHangUp" title="Hangup"><i class="fa fa-stop"></i></button>';
403
+                i += '</div>';
404
+            }
405
+            i += '</div>';
406
+
407
+            $('#sip-logitems').append(i);
408
+
409
+
410
+            // Start call timer on answer
411
+            if (item.status === 'answered') {
412
+                var tEle = document.getElementById(item.id);
413
+                ctxSip.callTimers[item.id] = new Stopwatch(tEle);
414
+                ctxSip.callTimers[item.id].start();
415
+            }
416
+
417
+            if (callActive && item.status !== 'ringing') {
418
+                ctxSip.callTimers[item.id].start({startTime : item.start});
419
+            }
420
+
421
+            $('#sip-logitems').scrollTop(0);
422
+        },
423
+
424
+        /**
425
+         * updates the call log ui
426
+         */
427
+        logShow : function() {
428
+
429
+            var calllog = JSON.parse(localStorage.getItem('sipCalls')),
430
+            x = [];
431
+
432
+            if (calllog !== null) {
433
+
434
+                $('#sip-splash').addClass('hide');
435
+                $('#sip-log').removeClass('hide');
436
+
437
+                // empty existing logs
438
+                $('#sip-logitems').empty();
439
+
440
+                // JS doesn't guarantee property order so
441
+                // create an array with the start time as
442
+                // the key and sort by that.
443
+
444
+                // Add start time to array
445
+                $.each(calllog, function(k,v) {
446
+                    x.push(v);
447
+                });
448
+
449
+                // sort descending
450
+                x.sort(function(a, b) {
451
+                    return b.start - a.start;
452
+                });
453
+
454
+                $.each(x, function(k, v) {
455
+                    ctxSip.logItem(v);
456
+                });
457
+
458
+            } else {
459
+                $('#sip-splash').removeClass('hide');
460
+                $('#sip-log').addClass('hide');
461
+            }
462
+        },
463
+
464
+        /**
465
+         * removes log items from localstorage and updates the UI
466
+         */
467
+        logClear : function() {
468
+
469
+            localStorage.removeItem('sipCalls');
470
+            ctxSip.logShow();
471
+        },
472
+
473
+        sipCall : function(target) {
474
+
475
+            try {
476
+                var s = ctxSip.phone.invite(target, {
477
+                    media : {
478
+                        stream      : ctxSip.Stream,
479
+                        constraints : { audio : true, video : false },
480
+                        render      : { remote: document.getElementById('audioRemote') }
481
+                        //render: { remote: $('#audioRemote').get()[0] }
482
+                        //RTCConstraints : { "optional": [{ 'DtlsSrtpKeyAgreement': 'true'} ]}
483
+                    }
484
+                });
485
+                s.direction = 'outgoing';
486
+                ctxSip.newSession(s);
487
+
488
+            } catch(e) {
489
+                throw(e);
490
+            }
491
+        },
492
+
493
+        sipTransfer : function(sessionid) {
494
+
495
+                var s  = ctxSip.Sessions[sessionid],
496
+                target = window.prompt('Enter destination number', '');
497
+
498
+            ctxSip.setCallSessionStatus('<i>Transfering the call...</i>');
499
+            s.refer(target);
500
+        },
501
+
502
+        sipHangUp : function(sessionid) {
503
+
504
+            var s = ctxSip.Sessions[sessionid];
505
+            // s.terminate();
506
+            if (!s) {
507
+                return;
508
+            } else if (s.startTime) {
509
+                s.bye();
510
+            } else if (s.reject) {
511
+                s.reject();
512
+            } else if (s.cancel) {
513
+                s.cancel();
514
+            }
515
+
516
+        },
517
+
518
+        sipSendDTMF : function(digit) {
519
+
520
+            try { ctxSip.dtmfTone.play(); } catch(e) { }
521
+
522
+            var a = ctxSip.callActiveID;
523
+            if (a) {
524
+                var s = ctxSip.Sessions[a];
525
+                s.dtmf(digit);
526
+            }
527
+        },
528
+
529
+        phoneCallButtonPressed : function(sessionid) {
530
+
531
+                var s  = ctxSip.Sessions[sessionid],
532
+                target = $("#numDisplay").val();
533
+
534
+            if (!s) {
535
+
536
+                $("#numDisplay").val("");
537
+                ctxSip.sipCall(target);
538
+
539
+            } else if (s.accept && !s.startTime) {
540
+
541
+                s.accept({
542
+                    media: {
543
+                            stream: ctxSip.Stream,
544
+                            constraints: { audio: true, video: false },
545
+                            render      : { remote: document.getElementById('audioRemote') }
546
+                            //render: { remote: $('#audioRemote').get()[0] }
547
+                            // RTCConstraints : { "optional": [{ 'DtlsSrtpKeyAgreement': 'true'} ]}
548
+                           }
549
+                });
550
+            }
551
+        },
552
+
553
+        phoneMuteButtonPressed : function (sessionid) {
554
+
555
+            var s = ctxSip.Sessions[sessionid];
556
+
557
+            if (!s.isMuted) {
558
+                s.mute();
559
+            } else {
560
+                s.unmute();
561
+            }
562
+        },
563
+
564
+        phoneHoldButtonPressed : function(sessionid) {
565
+
566
+            var s = ctxSip.Sessions[sessionid];
567
+
568
+            if (s.isOnHold().local === true) {
569
+                s.unhold();
570
+            } else {
571
+                s.hold();
572
+            }
573
+        },
574
+
575
+
576
+        setError : function(err, title, msg, closable) {
577
+
578
+            // Show modal if err = true
579
+            if (err === true) {
580
+                $("#mdlError p").html(msg);
581
+                $("#mdlError").modal('show');
582
+
583
+                if (closable) {
584
+                    var b = '<button type="button" class="close" data-dismiss="modal">&times;</button>';
585
+                    $("#mdlError .modal-header").find('button').remove();
586
+                    $("#mdlError .modal-header").prepend(b);
587
+                    $("#mdlError .modal-title").html(title);
588
+                    $("#mdlError").modal({ keyboard : true });
589
+                } else {
590
+                    $("#mdlError .modal-header").find('button').remove();
591
+                    $("#mdlError .modal-title").html(title);
592
+                    $("#mdlError").modal({ keyboard : false });
593
+                }
594
+                $('#numDisplay').prop('disabled', 'disabled');
595
+            } else {
596
+                $('#numDisplay').removeProp('disabled');
597
+                $("#mdlError").modal('hide');
598
+            }
599
+        },
600
+
601
+        /**
602
+         * Tests for a capable browser, return bool, and shows an
603
+         * error modal on fail.
604
+         */
605
+        hasWebRTC : function() {
606
+
607
+            if (navigator.webkitGetUserMedia) {
608
+                return true;
609
+            } else if (navigator.mozGetUserMedia) {
610
+                return true;
611
+            } else if (navigator.getUserMedia) {
612
+                return true;
613
+            } else {
614
+                ctxSip.setError(true, 'Unsupported Browser.', 'Your browser does not support the features required for this phone.');
615
+                window.console.error("WebRTC support not found");
616
+                return false;
617
+            }
618
+        }
619
+    };
620
+
621
+    userSIPPass = '';
622
+    window.opener.sipUserPasswd = '';
623
+
624
+    // Throw an error if the browser can't hack it.
625
+    if (!ctxSip.hasWebRTC()) {
626
+        return true;
627
+    }
628
+
629
+    ctxSip.phone = new SIP.UA(ctxSip.config);
630
+
631
+    ctxSip.phone.on('connected', function(e) {
632
+        ctxSip.setStatus("Connected");
633
+    });
634
+
635
+    ctxSip.phone.on('disconnected', function(e) {
636
+        ctxSip.setStatus("Disconnected");
637
+
638
+        // disable phone
639
+        ctxSip.setError(true, 'Websocket Disconnected.', 'An Error occurred connecting to the websocket.');
640
+
641
+        // remove existing sessions
642
+        $("#sessions > .session").each(function(i, session) {
643
+            ctxSip.removeSession(session, 500);
644
+        });
645
+    });
646
+
647
+    ctxSip.phone.on('registered', function(e) {
648
+
649
+        var closeEditorWarning = function() {
650
+            return 'If you close this window, you will not be able to make or receive calls from your browser.';
651
+        };
652
+
653
+        var closePhone = function() {
654
+            // stop the phone on unload
655
+//            localStorage.removeItem('ctxPhone');
656
+            localStorage.removeItem('SipTripPhone');
657
+            ctxSip.phone.stop();
658
+        };
659
+
660
+        window.onbeforeunload = closeEditorWarning;
661
+        window.onunload       = closePhone;
662
+
663
+        // This key is set to prevent multiple windows.
664
+        localStorage.setItem('SipTripPhone', 'true');
665
+
666
+        $("#mldError").modal('hide');
667
+        ctxSip.setStatus("Ready");
668
+
669
+        // Get the userMedia and cache the stream
670
+        var audio = document.getElementById('audioRemote');
671
+        var mediaStream = new MediaStream();
672
+        let audioTrack = null;
673
+
674
+        navigator.mediaDevices.getUserMedia({ audio : true, video : false }, ctxSip.getUserMediaSuccess, ctxSip.getUserMediaFailure).then(function(mediaStream) {
675
+
676
+           let audioTracks = mediaStream.getAudioTracks();
677
+           audio.srcObject = mediaStream;
678
+
679
+           if (audioTracks.length) {
680
+               audioTrack = audioTracks[0];
681
+           }
682
+        }).then(function() {
683
+           new Promise(function(resolve) {
684
+               audio.onloadedmetadata = resolve;
685
+           })
686
+        })
687
+
688
+    });
689
+
690
+    ctxSip.phone.on('registrationFailed', function(e) {
691
+        ctxSip.setError(true, 'Registration Error.', 'An Error occurred registering your phone. Check your settings.');
692
+        ctxSip.setStatus("Error: Registration Failed");
693
+    });
694
+
695
+    ctxSip.phone.on('unregistered', function(e) {
696
+        ctxSip.setError(true, 'Registration Error.', 'An Error occurred registering your phone. Check your settings.');
697
+        ctxSip.setStatus("Error: Registration Failed");
698
+    });
699
+
700
+    ctxSip.phone.on('invite', function (incomingSession) {
701
+
702
+        var s = incomingSession;
703
+
704
+        s.direction = 'incoming';
705
+        ctxSip.newSession(s);
706
+    });
707
+
708
+    // Auto-focus number input on backspace.
709
+    $('#sipClient').keydown(function(event) {
710
+        if (event.which === 8) {
711
+            $('#numDisplay').focus();
712
+        }
713
+    });
714
+
715
+    $('#numDisplay').keypress(function(e) {
716
+        // Enter pressed? so Dial.
717
+        if (e.which === 13) {
718
+            ctxSip.phoneCallButtonPressed();
719
+        }
720
+    });
721
+
722
+    var clck = 0;
723
+
724
+    $('.digit').click(function(event) {
725
+
726
+     if (event.shiftKey) {
727
+
728
+         clck++;
729
+         event.preventDefault();
730
+         var num = $('#numDisplay').val();
731
+         var dig;
732
+         var diginit = $(this).data('digit').toString().split(',');
733
+         var elct = diginit.length;
734
+
735
+         dig = diginit[clck%elct];
736
+         var numsec = num.slice(0,-1);
737
+         $('#numDisplay').val(numsec+dig);
738
+         ctxSip.sipSendDTMF(dig);
739
+
740
+     } else {
741
+         event.preventDefault();
742
+         var num = $('#numDisplay').val();
743
+         var dig;
744
+         var diginit = $(this).data('digit').toString().split(',');
745
+
746
+         dig = diginit[0];
747
+         clck = 0;
748
+         $('#numDisplay').val(num+dig);
749
+         ctxSip.sipSendDTMF(dig);
750
+       }
751
+
752
+       return false;
753
+
754
+    });
755
+
756
+    $('#phoneUI .dropdown-menu').click(function(e) {
757
+        e.preventDefault();
758
+    });
759
+
760
+    $('#phoneUI').delegate('.btnCall', 'click', function(event) {
761
+        ctxSip.phoneCallButtonPressed();
762
+        // to close the dropdown
763
+        return true;
764
+    });
765
+
766
+    $('.sipLogClear').click(function(event) {
767
+        event.preventDefault();
768
+        ctxSip.logClear();
769
+    });
770
+
771
+    $('#sip-logitems').delegate('.sip-logitem .btnCall', 'click', function(event) {
772
+        var sessionid = $(this).closest('.sip-logitem').data('sessionid');
773
+        ctxSip.phoneCallButtonPressed(sessionid);
774
+        return false;
775
+    });
776
+
777
+    $('#sip-logitems').delegate('.sip-logitem .btnHoldResume', 'click', function(event) {
778
+        var sessionid = $(this).closest('.sip-logitem').data('sessionid');
779
+        ctxSip.phoneHoldButtonPressed(sessionid);
780
+        return false;
781
+    });
782
+
783
+    $('#sip-logitems').delegate('.sip-logitem .btnHangUp', 'click', function(event) {
784
+        var sessionid = $(this).closest('.sip-logitem').data('sessionid');
785
+        ctxSip.sipHangUp(sessionid);
786
+        return false;
787
+    });
788
+
789
+    $('#sip-logitems').delegate('.sip-logitem .btnTransfer', 'click', function(event) {
790
+        var sessionid = $(this).closest('.sip-logitem').data('sessionid');
791
+        ctxSip.sipTransfer(sessionid);
792
+        return false;
793
+    });
794
+
795
+    $('#sip-logitems').delegate('.sip-logitem .btnMute', 'click', function(event) {
796
+        var sessionid = $(this).closest('.sip-logitem').data('sessionid');
797
+        ctxSip.phoneMuteButtonPressed(sessionid);
798
+        return false;
799
+    });
800
+
801
+    $('#sip-logitems').delegate('.sip-logitem', 'dblclick', function(event) {
802
+        event.preventDefault();
803
+
804
+        var uri = $(this).data('uri');
805
+        $('#numDisplay').val(uri);
806
+        ctxSip.phoneCallButtonPressed();
807
+    });
808
+
809
+    $('#sldVolume').on('change', function() {
810
+
811
+            var v  = $(this).val() / 100,
812
+            btn    = $('#btnVol'),
813
+            icon   = $('#btnVol').find('i'),
814
+            active = ctxSip.callActiveID;
815
+
816
+        // Set the object and media stream volumes
817
+        if (ctxSip.Sessions[active]) {
818
+            ctxSip.Sessions[active].player.volume = v;
819
+            ctxSip.callVolume                     = v;
820
+        }
821
+
822
+        // Set the others
823
+        $('audio').each(function() {
824
+            $(this).get()[0].volume = v;
825
+        });
826
+
827
+        if (v < 0.1) {
828
+            btn.removeClass(function (index, css) {
829
+                   return (css.match (/(^|\s)btn\S+/g) || []).join(' ');
830
+                })
831
+                .addClass('btn btn-sm btn-danger');
832
+            icon.removeClass().addClass('fa fa-fw fa-volume-off');
833
+        } else if (v < 0.8) {
834
+            btn.removeClass(function (index, css) {
835
+                   return (css.match (/(^|\s)btn\S+/g) || []).join(' ');
836
+               }).addClass('btn btn-sm btn-info');
837
+            icon.removeClass().addClass('fa fa-fw fa-volume-down');
838
+        } else {
839
+            btn.removeClass(function (index, css) {
840
+                   return (css.match (/(^|\s)btn\S+/g) || []).join(' ');
841
+               }).addClass('btn btn-sm btn-primary');
842
+            icon.removeClass().addClass('fa fa-fw fa-volume-up');
843
+        }
844
+        return false;
845
+    });
846
+
847
+    // Hide the spalsh after 3 secs.
848
+    setTimeout(function() {
849
+        ctxSip.logShow();
850
+    }, 3000);
851
+
852
+
853
+    /**
854
+     * Stopwatch object used for call timers
855
+     *
856
+     * @param {dom element} elem
857
+     * @param {[object]} options
858
+     */
859
+    var Stopwatch = function(elem, options) {
860
+
861
+        // private functions
862
+        function createTimer() {
863
+            return document.createElement("span");
864
+        }
865
+
866
+        var timer = createTimer(),
867
+            offset,
868
+            clock,
869
+            interval;
870
+
871
+        // default options
872
+        options           = options || {};
873
+        options.delay     = options.delay || 1000;
874
+        options.startTime = options.startTime || Date.now();
875
+
876
+        // append elements
877
+        elem.appendChild(timer);
878
+
879
+        function start() {
880
+            if (!interval) {
881
+                offset   = options.startTime;
882
+                interval = setInterval(update, options.delay);
883
+            }
884
+        }
885
+
886
+        function stop() {
887
+            if (interval) {
888
+                clearInterval(interval);
889
+                interval = null;
890
+            }
891
+        }
892
+
893
+        function reset() {
894
+            clock = 0;
895
+            render();
896
+        }
897
+
898
+        function update() {
899
+            clock += delta();
900
+            render();
901
+        }
902
+
903
+        function render() {
904
+            timer.innerHTML = moment(clock).format('mm:ss');
905
+        }
906
+
907
+        function delta() {
908
+            var now = Date.now(),
909
+                d   = now - offset;
910
+
911
+            offset = now;
912
+            return d;
913
+        }
914
+
915
+        // initialize
916
+        reset();
917
+
918
+        // public API
919
+        this.start = start; //function() { start; }
920
+        this.stop  = stop; //function() { stop; }
921
+    };
922
+
923
+});