Reference12r2:Concept myPBX Toolbox

From innovaphone wiki
Jump to navigation Jump to search
There are also other versions of this article available: Reference12r1 | Reference12r2 (this version) | Reference13r1

The myPBX toolbox is a set of Java Script files that can be used for integrating functionality of the innovaphone PBX into arbitrary web sites.

Supported features

  • Presence monitoring
  • Outgoing WebRTC calls with audio, video and application sharing

Requirements

Download
Web site
  • Any web server with https support should be compatible with the toolbox. The JavaScript library files have to be included in the web site
PBX
  • User object with password for login and a WebRTC device
  • Call filters for that user object
  • Valid STUN/TURN configuration
The TURN credentials are transferred in clear text to the browser. This is by design of the WebRTC clients. 
To avoid disclosure of credentials matching for other services define additional username/password combination on the TURN server that is used by WebRTC clients only.
Licenses
  • myPBX license
  • Video license (for video telephony)
  • Application Sharing license (for viewing shared applications)
  • WebRTC channel license (per WebRTC call)
Note: The WebRTC channel license must be activated on each indiviual PBX by configuring a number of "Max WebRTC calls".
Network
  • The PBX service (https://pbx.sample.domain/PBX0/WEBSOCKET) must be accessible from the public internet (NAT port forwarding or reverse proxy).
Certificates
  • Flow:
The caller (more precisely, the web browser used by the user accessing your web site) will create a websocket TLS connection to your PBX.
So the browser needs to trust the PBX's (https://pbx.sample.domain) certificate.
As the web site probably shall be usable by any client out there on the internet, you can not control the calling browsers certificate trust list.
  • Requirement:
You must install a valid certificate created by a well-known certificate authority on your PBX.
If you use a reverse proxy you have to add the certificate on the reverse proxy as device certificate.

Authentication

For connecting the credentials of a PBX user account is needed. The credentials can be hard coded in the Java Script code (not recommended) or they can be stored on the web server that hosts the application (recommended).

A typical flow for the second method would be like follows.

  1. The PBX chooses the server parameters (realm, sessionId, serverNonce) and sends it to the Application
  2. The callback onAuthenticate(realm, sessionId, serverNonce) is called.
  3. The Application sends the parameters to the web server.
  4. The Webserver chooses the client parameters (username, clientNonce) and calculated the login digest and sends it back to the application.
  5. The application calls the setAuthentication(username, clientNonce, digest) function.
  6. The PBX accepts the login.

Reference

innovaphone.pbxwebsocket.Connection

Used to create a connection to the PBX for presence subscription.

Includes

The following files have to be included in the html file in order to use the WebRTC endpoint.

  • innovaphone.common.crypto.js
  • innovaphone.pbxwebsocket.Connection.js

Constructor

new Connection(url, username, password)
url
The URL of the PBX websocket service (e.g. wss://xxx/PBX0/WEBSOCKET/websocket)
username
The username of the PBX user object.
password
The password of the PBX user object.
you can set the following members to define callback functions provided by your web page:
connection.onauthenticate
called when the login authentication needs to be computed with the parameters (realm, sessionId, serverNonce). Applications should calculate the login hash using these parameters and the username and password and call setAuthentication.
connection.onconnected
called when the call is connected
connection.onerror
called on error
connection.onclosed
called when the call is closed
connection.onendpointpresence
called when a subscribed users' presence information changes

Methods

function close()
Disconnects from the PBX.
function setAuthentication(username, clientNonce, digest)
Sets the client parameters of the authentication and the calculated digest.
username
The H323 id of the user object
clientNonce
A random number
digest
sha256("innovaphonePbxWebsocket:ClientAuth:" + realm + ":" + sessionId + ":" + username + ":" + password + ":" + clientNonce + ":" + serverNonce)
function sendSubscribeEndpoint(name, number)
Starts presence monitoring for a given endpoint specifyed by name or number.
name
The URI or H.323 id
number
The phone number
function sendUnsubscribeEndpoint(name, number)
Stops presence monitoring for a given endpoint specifyed by name or number.
name
The URI or H.323 id
number
The phone number

The methods for WebRTC calls are not described here. Use innovaphone.pbxwebsocket.WebRtcEndpoint.js instead.

Callbacks

function onauthenticate(realm, sessionId, serverNonce)
This callback is used for authenting users when the web page shall not be aware of the user password. Set connection.onauthenticate to get a callback during login process. The login hash can then be calculated on the webserver and be given back using function setAuthentication.
function onconnected(userInfo)
Called when the connection has been successfully established.
userInfo
An object containing informmation about the connected user.
function onerror(error)
Called when the connection could not be established or an other error occurred.
error
A string containing an error message.
function onclosed()
Called if the connection was closed.
function onendpointpresence(name, number, phoneStatus, imStatus, activity, note)
Called if the presence of a subscribed endpoint has changed.
name
The URI or H.323 id
number
The phone number
phoneStatus
Tells if a phone is registered, open or closed.
imStatus
Tells if a chat client is registered, open or closed.
activity
Presence activity away, busy, lunch, vacation, busy, dnd, on-the-phone
note
Presence note

The callbacks for WebRTC calls are not described here. Use innovaphone.pbxwebsocket.WebRtcEndpoint.js instead.

innovaphone.pbxwebsocket.WebRtcEndpoint

This file contains a WebRTC endpoint implementation that can be used for adding WebRTC calls to a web page. For that the credentials of a user object on the PBX is needed. Visitors of the web page will use that user object for making phone calls.

In some browsers like Chrome WebRTC only works on HTTPS pages. So it is mandatory to use HTTPS and WSS on your page.

The WebRtcEndpoint is derived from the Connection described above. As such, all interfaces defined for those are also available, plus some more.

Includes

The following files have to be included in the html file in order to use the WebRTC endpoint.

  • innovaphone.common.crypto.js
  • innovaphone.pbxwebsocket.Connection.js
  • innovaphone.pbxwebsocket.ToneGenerator.js
  • innovaphone.pbxwebsocket.WebRtcEndpoint.js

If you want to use application sharing the following files must also be included:

  • innovaphone.applicationSharing.jpeg.js
  • innovaphone.applicationSharing.zlib.js
  • innovaphone.applicationSharing.png.js
  • innovaphone.applicationSharing.main.js

Additionally the MP3 files containing the ring tones and ring back tones are needed on the web server in the same directory as the javascript files.

Compatibility check

You can test if the browser supports WebRTC by checking the bool innovaphone.pbxwebsocket.WebRtc.supported.

Constructor

new WebRtcEndpoint(url, username, password, device, physicalLocation, regContext, onLog, onCall, onAuthenticate)
url
The URL of the PBX websocket service (e.g. ws://10.0.0.1/PBX0/WEBSOCKET/websocket)
username
The username of the PBX user object.
password
The password of the PBX user object.
device
The device ID that shall be used for making calls.
physicalLocation
The physical location of the user (optional).
regContext
must be set to null.
logFunction
A callback function that is called for logging debug info (optional).
onCall
A callback function that is called when calls are added, updated or removed. Applications that want to use call control have to specify this callback function. Applications that don't should give null. (optional)
onAuthenticate
An optional callback function. If set it is called with the parameters (realm, sessionId, serverNonce). Applications should calculate the login hash using these parameters and the username and password and call setAuthentication.

Methods

function close()
Closes the WebRTC endpoint and disconnects from the PBX.
function initCall(name, number, video, sharing)
Starts a phone call. This is only possible if the application supplied an onCall callback to the constructor.
name
The URI (e.g. Name in the PBX User object) to be called (optional, supply name or number).
number
The phone number to be called (optional, supply name or number).
video
Set to true for starting a video call (optional).
sharing
Set to true for starting an application sharing call (optional).
function connectCall(id)
Connects a call.
id
The ID of the call.
function dtmfCall(id, digits)
Sends dtmf digits to a connected call.
id
The ID of the call.
digits
A string containing DTMF digits (0-9,*,#,A,B,C,D).
function clearCall(id)
Terminates a call.
id
The ID of the call (optional). If no ID is supplied, all calls are terminated.
function attachVideo(local, remote)
Attaches HTML video elements to the WebRTC endpoints. The endpoint will use them to playback video. Applications can both attach before or during a video call. Also attaching multiple video elements for a single call is possible. The application should mute the video elements (muted="muted") in order to avoid playback of audio.
local
The HTML video element for playback of the local webcam image (optional, may be null).
remote
The HTML video element for playback of the remote video image (optional, may be null).
function detachVideo(local, remote)
Detaches HTML video elements that have previously attached. This will stop the playback on the supplied elements.
local
The HTML video element to be detached (optional, may be null).
remote
The HTML video element to be detached (optional, may be null).
function attachSharing(sharingDiv, createAppCallback, removeAppCallback, resizeCalback)
Attaches a DIV element to the WebRTC endpoints. The endpoint will allocate inside this element a canvas object where the application sharing data will be displayed. This DIV element must have as style position:relative, otherwise the mouse will not be displayed correctly. Since more than one application could be shared, the javascript application has to provide a callback to be informed that a new application arrived and another callback to be informed that an application is not anymore shared. Both callbacks should received an id as argument. Another callback will be provided in case the shared application changes its resolution. It is also possible that the JS application receives data from different participants, for instance during a conference. That is why this sender_id is also needed.
createAppCallback
createAppCallback(sender_id, id, name). This callback also receives the name of the new application.
removeAppCallback
removeAppCallback(sender_id, id).
resizeCallback
resizeCallback().
function detachSharing()
Detaches elements that were previously attached.
function sharingEvent(type, data)
The endpoint provides an interface to send events to the application sharing class.
changeDisplaySender
This event is used to switch between the users which are sharing something. Useful in a conference. type is equal to changeDisplaySender and data should be the id of the sender. This id is the one provided in the create application callback.
changeDisplayApp
This event is used to switch inside the canvas object between applications being shared. type is equal to changeDisplayApp and data should be the id of the application to be displayed. This id is the one provided in the create application callback.
fitToElement
This event adjusts the application being shared to the size of the DIV element provided with the attachSharing function. type is equal to fitToElement and data should be true if the application should be adjusted to the canvas element or false otherwise (original size).
requestControl
This event allows the client to request control (mouse, keyboard) over the shared applications. The sharing party still must accept this request before the client gets the control. type is equal to requestControl and data should be null.
function setAuthentication(username, clientNonce, digest)
Sets the client parameters of the authentication and the calculated digest.
username
The H323 id of the user object
clientNonce
A random number
digest
sha256("innovaphonePbxWebsocket:ClientAuth:" + realm + ":" + sessionId + ":" + username + ":" + password + ":" + clientNonce + ":" + serverNonce)

Callbacks

function onLog(text)
Should write the supplied text to the log of the application.
function onCall(event, call)
Is called when the state of calls of this WebRTC endpoint changes.
event
A string that can be added, updated and removed.
call
The call object containing the current info about the call.
id
The numeric id of the call.
dir
The direction of the call (in or out).
state
The state of the call (idle, calling, incomplete, complete, alerting, connected, disconnecting, disconnected, parked).
hold
true, if the call is on hold (optional).
name
The URI of the remote party (optional).
number
The phone number of the remote party (optional).
video
true, if video is active (optional).
sharing
true, if application sharing is active (optional).
cause
The cause code (optional).

Example

Usage

This sample code uses the innovaphone demo PBX , which is publicly available, so you don't need to setup your own PBX to test the code.

It will create a WebRTC endpoint and register it as user extern-web at the demo PBX. The following options are provided:

Log Toolbox
turns on internal toolbox logging if checked
Log Web Site
turns on web-site logging if checked
Actions
call control actions:
START
initialize the web application
OFF_HOOK
start a WebRTC call to the extension given in the input field Target number
ON_HOOK
drop an active WebRTC call
END
dispose all objects used in the web application
Target number
defines the extension to be called on OFF_HOOK. Also, a presence subscription will be established to this extension on START. The current presence status is displayed next to this field.
Despite ist name, you also can enter a SIP URI (i.e. Name) to be called
Local Video
calls are done with video support if checked
Sharing
calls are done with application sharing support if checked

Using a different PBX

You can change some configuration parameters by modifying var config in the sample code.

Using different STUN/TURN Servers

The toolbox code will retrieve and use the STUN and TURN server settings from the PBX (in IP4/General/STUN). You can not override that using the toolbox code. When you use a different PBX, make sure those settings are well configured there.

Debugging STUN/TURN

Issues with your STUN and/or TURN server are best debugged using the browser's specific diagnostic tools:

Chrome
chrome://webrtc-internals/
Firefox
about:webrtc

Sending additional Data to the called Destination

When calling a name, you can add additional information to it using the ? syntax. I.e. to send the the information user=Müller when calling user user-1, you can call user-1?user=M%C3%BCller. This information will be passed to the called endpoint. This information can also be retrieved when monitoring the calling WebRTC endpoint with SOAP (you will see it in the h323 property of the peer-No entry in the CallInfo record received with Poll()).

To see where you are connected to, you can look at the name and number properties of the onCall callback with event = updated and state = connected.

Known Issues

untrusted PBX certificate
The code will open a secure web socket connection to the PBX. This connection will thus use TLS in any case and some browsers will refuse to open the connection if the certificate of the target server (in this case, the innovaphone Demo PBX) is not trusted. You will likely see a message such as web: onPresenceError(WEBSOCKET_ERROR). Try to create a security exception in your browser for the PBX (for example, connect to https://demo.innovaphone.com, confirm access and save this permanently).
In Chrome and Firefox, you can permanently save the security exception directly from within the browser.
In Edge, you must install the certificate in to the windows system:
  • open http://demo.innovaphone.com/
  • navigate to General/Certificates
  • right-click on the DER link next to the IP811-41-06-f1 certificate in the Trust list
  • save this file to your hard disk
  • in file explorer, right click on the IP811-41-06-f1.cer file
  • select Install certificate
  • in the wizard, select current user, Store certificate in this storage, trusted Certificate Authorities (exact wording depends on windows version and language)
Internet Explorer and Safari do not support WebRTC anyway (October 2017)
WebRTC not supported
older browsers do not support WebRTC. You will see the message sorry, WebRTC not supported by your browser. Use another browser

Sample Code

Toolkit Library

The required files can be obtained from the download website (V12r1 and up) in the toolbox package. Unzip innovaphone-jslib.zip in to an empty directory on your web server. For testing, you can also copy the files to your local hard disk.

Sample Application

Save the following HTML code as index.html in to the directory you saved the toolbox library to and open it with your browser from there. Some browser require HTTPS to use WebRTC, so be sure to open it with https://.

The sample code and the toolbox library files are also installed on the demo PBX. You can open https://demo.innovaphone.com/ and navigate to General/SSD/Browse content (HTML)/toolbox/index.html.

<html>

   <head>
       <title>innovaphone WebRTC Demo</title>
       <meta charset="UTF-8">
       <meta name="viewport" content="width=device-width, initial-scale=1.0">
       <link rel="shortcut icon" href="logo.gif">
       <script type="text/javascript" src="innovaphone.common.crypto.js"></script>
       <script type="text/javascript" src="innovaphone.pbxwebsocket.Connection.js"></script>
       <script type="text/javascript" src="innovaphone.pbxwebsocket.ToneGenerator.js"></script>
       <script type="text/javascript" src="innovaphone.applicationSharing.jpeg.js"></script>
       <script type="text/javascript" src="innovaphone.applicationSharing.zlib.js"></script>
       <script type="text/javascript" src="innovaphone.applicationSharing.png.js"></script>
       <script type="text/javascript" src="innovaphone.applicationSharing.main.js"></script>
       <script type="text/javascript" src="innovaphone.pbxwebsocket.WebRtcEndpoint.js"></script>
       <script type="text/javascript">
           var config = {
               url: "wss://demo.innovaphone.com/PBX0/WEBSOCKET/websocket",
               username: "extern-web",
               password: "demo",
               device: "extern-web",
               physicalLocation: null,
               regContext: null
           };
           var WebRtcEndpoint = innovaphone.pbxwebsocket.WebRtc.Endpoint;
           var Connection = innovaphone.pbxwebsocket.Connection;
           var endpoint = null;
           var presence = null;
           var mycall = null;
           var t0 = new Date().getTime();
           var sha256 = innovaphone.common.crypto.sha256;
           function getTimeSinceInit() {
               var ts = new Date().getTime() - t0;
               return ((ts / 1000) | 0) + "." + ts % 1000;
           }
           function toolkitLog(text) {
               _log("toolkit: " + text, "toolboxdebug");
           }
           function log(text) {
               _log("web: " + text, "webdebug");
           }
           function _log(text, type) {
               text = getTimeSinceInit() + ": " + text;
               console.log(text);
               var flag = document.getElementById(type);
               if (flag && flag.checked) {
                   var logdiv = document.getElementById("logdiv");
                   if (logdiv) {

logdiv.innerHTML = "

" + text + "

" + logdiv.innerHTML;

                   }
               }
           }
           // receive call related events
           function onCall(event, call) {
               if (event == "added")
                   mycall = call.id;
               else if (event == "removed") {
                   mycall = null;
               } else if (call.state == "disconnecting") {
                   endpoint.clearCall(call.id);
               }
               if (call.dir == "in") {
                   if (endpoint) {
                       log("rejecting incoming call");
                       endpoint.clearCall(call.id);
                   }
               }
               log("Call " + event + ": " + JSON.stringify(call));
           }
           function onAuthenticatePresence(realm, sessionId, serverNonce) {
               log("onAuthenticatePresence(" + realm + ", " + sessionId + ", " + serverNonce + ")");
               doLogin(presence, realm, sessionId, serverNonce);
           }
           function onAuthenticateCall(realm, sessionId, serverNonce) {
               log("onAuthenticateCall(" + realm + ", " + sessionId + ", " + serverNonce + ")");
               doLogin(endpoint, realm, sessionId, serverNonce);
           }
           /*
            * compute authentication
            * you can calculate your digest inline (as we do here, same as not specifiying the onAuthenticate at all) 
            * or you can have a server-side script calculate it for slightly better security
            * (see WebRtcEndpoint.html for a method to do that)
            */
           function doLogin(connection, realm, sessionId, serverNonce) {
               var clientNonce = 0;
               while (clientNonce == 0) {
                   clientNonce = Math.ceil(Math.random() * 0xffffffff);
               }
               connection.setAuthentication(
                       config.username,
                       clientNonce,
                       sha256("innovaphonePbxWebsocket:ClientAuth:" + realm + ":" + sessionId + ":" + config.username + ":" + config.password + ":" + clientNonce + ":" + serverNonce)
                       );
           }


           // receive application sharing canvas resizing
           function resizeCanvas() {
               log("resizing canvas(ignored)");
           }
           // receive remote endpoint presence 
           function onEndpointPresence(name, number, phoneStatus, imStatus, activity, note) {
               log("presence: name=" + name + " number=" + number + " phoneStatus=" + phoneStatus + " imStatus=" + imStatus + " activity=" + activity + " note=" + note);
               var status = document.getElementById("presence");
               if (status)
                   status.innerHTML = " phoneStatus=" + phoneStatus + " imStatus=" + imStatus + " activity=" + activity + " note=" + note;
           }
           // create new application sharing session
           function createNewApplication(sender_id, id, name) {
               var iid = "appSharing_" + sender_id + "_" + id;
               log("createNewApplication(" + sender_id + ", " + id + ", " + name + "), id=" + iid);
               if (document.getElementById(iid))
                   return;
               var new_app = document.createElement("input");
               new_app.setAttribute("id", iid);
               new_app.setAttribute("value", name + "(" + iid + ")");
               new_app.setAttribute("type", "button");
               new_app.onclick = function () {
                   changeDisplayApp(iid);
               };
               var sl = document.getElementById("sharing-local");
               if (sl) {
                   sl.appendChild(new_app);
                   changeDisplayApp(iid);
               } else
                   log("no 'sharing-local' element");
           }
           // switch to given application sharing session
           function changeDisplayApp(id) {
               log("changeDisplayApp(" + id + ")");
               var input_b = document.getElementById(id);
               if (input_b) {
                   var sl = document.getElementById("sharing-local");
                   if (sl) {
                       var ci;
                       for (ci = 0; ci < sl.children.length; ci++) {
                           if (sl.children[ci].id.substring(0, 11) == "appSharing_") {
                               sl.children[ci].style.backgroundColor = null;
                               sl.children[ci].disabled = false;
                           }
                       }
                   }
                   input_b.style.backgroundColor = "LightGreen";
                   input_b.disabled = true;
                   var s_app_id = String(id); // id == appSharing_x_y where x is the sender and y the application, both are numbers, 0, 1, 2, ..
                   var a_app_id = s_app_id.split("_"); // a_app_id is an array with three elements.
                   endpoint.sharingEvent('changeDisplaySender', a_app_id[1]);
                   endpoint.sharingEvent('changeDisplayApp', a_app_id[2]);
               }
           }
           // remove application sharing session
           function removeApplication(sender_id, id) {
               var iid = "appSharing_" + sender_id + "_" + id;
               log("removeApplication(" + sender_id + ", " + id + "), id=" + iid);
               var input_b = document.getElementById(iid);
               if (input_b) {
                   var sl = document.getElementById("sharing-local");
                   if (sl)
                       sl.removeChild(input_b);
                   else
                       log("no 'sharing-local' element");
               } else
                   log("no '" + "appSharing_" + sender_id + "_" + id + "' element");
           }
           // create a presence subscription
           function startPresence() {
               if (presence)
                   endPresence();
               var number = document.getElementById("number");
               if (number && number.value && number.value.trim() != "") {
                   presence = new Connection(config.url, null, null);
                   presence.onauthenticate = onAuthenticatePresence;
                   presence.onconnected = onPresenceConnected;
                   presence.onerror = onPresenceError;
                   presence.onclosed = onPresenceClosed;
                   presence.onendpointpresence = onEndpointPresence;
               }
           }
           function onPresenceConnected(userInfo) {
               log("onPresenceConnected(" + JSON.stringify(userInfo) + ")");
               var number = document.getElementById("number");
               if (number && number.value && number.value.trim() != "") {
                   var n = number.value.trim();
                   var e164 = (!isNaN(parseFloat(n)) && isFinite(n)) ? number.value.trim() : null;
                   var sipuri = (!isNaN(parseFloat(n)) && isFinite(n)) ? null : number.value.trim();
                   log("starting presence subscription for " + sipuri + "/" + e164);
                   presence.sendSubscribeEndpoint(sipuri, e164);
               }
           }
           function onPresenceError(error) {
               log("onPresenceError(" + error + ")");
           }
           function onPresenceClosed() {
               log("onPresenceClosed");
               presence = null;
           }
           // end presence subscription
           function endPresence() {
               if (presence) {
                   log("ending presence");
                   presence.close();
                   var status = document.getElementById("presence");
                   if (status)
                       status.innerHTML = "no presence available";
               }
           }
           // initialize page code
           function start() {
               // create a WebRTC endpoint
               if (!endpoint) {
                   if (!innovaphone.pbxwebsocket.WebRtc.supported) {
                       log("sorry, WebRTC not supported by your browser");
                   } else {
                       t0 = new Date().getTime();
                       log("start()");
                       configdiv = document.getElementById("config");
                       if (configdiv) {
                           var ui = config.url.replace(/^wss?:\/\/([^/]*).*/i, "https://$1");
                           configdiv.innerHTML = "Config: user=" + config.username + ", device=" + config.device + ", WS=" + config.url + ", PBX-UI=<a href='" + ui + "'>" + ui + "</a>";
                       }
                       endpoint = new WebRtcEndpoint(config.url, config.username, config.password, config.device, config.physicalLocation, config.regContext, toolkitLog, onCall, onAuthenticateCall);
                       endpoint.attachVideo(document.getElementById("video-local"), document.getElementById("video-remote"));
                       endpoint.attachSharing(document.getElementById("sharing-local"), createNewApplication, removeApplication, resizeCanvas);
                   }
               } else {
                   log("WebRTC endpoint already started");
               }
               // create presence subscription
               if (!presence)
                   startPresence();
               else {
                   log("presence subscription already started");
               }
           }
           // finalize page code
           function end() {
               if (endpoint) {
                   if (mycall)
                       onhook();
                   log("closing endpoint");
                   endpoint.detachVideo(document.getElementById("video-local"), document.getElementById("video-remote"));
                   endpoint.detachSharing();
                   endpoint.close();
                   endpoint = null;
               } else {
                   log("no endpoint to end");
               }
               if (presence) {
                   log("closing subscription");
                   presence.close();
               } else {
                   log("no subscription to end");
               }
           }
           // initiate call
           function offhook() {
               if (endpoint) {
                   if (mycall != null) {
                       log("already off hook");
                   } else {
                       var number = document.getElementById("number");
                       if (number && number.value && number.value.trim() != "") {
                           var n = number.value.trim();
                           var e164 = (!isNaN(parseFloat(n)) && isFinite(n)) ? number.value.trim() : null;
                           var sipuri = (!isNaN(parseFloat(n)) && isFinite(n)) ? null : number.value.trim();
                           var usevideo = document.getElementById("usevideo");
                           usevideo = (usevideo && usevideo.checked);
                           var usesharing = document.getElementById("usesharing");
                           usesharing = (usesharing && usesharing.checked);
                           log("calling " + sipuri + "/" + e164 + (usevideo ? " with " : " no ") + " video, " + (usesharing ? " with " : " no ") + " sharing ...");
                           endpoint.initCall(sipuri, e164, usevideo, usesharing); // we always call nmubers, no names in this demo
                       } else {
                           log("Please specify Target number");
                       }
                   }
               } else {
                   log("not started");
               }
           }
           // terminate call
           function onhook() {
               if (mycall) {
                   log("closing call " + mycall);
                   if (endpoint)
                       endpoint.clearCall(mycall);
               } else {
                   log("not off hook");
               }
           }
       </script>
   </head>
   <body onunload="end();">
       Log Toolbox: <input type="checkbox" id="toolboxdebug"/>
       Log Web Site: <input type="checkbox" id="webdebug" checked="checked"/>

Actions:
           <a onclick="start();
                   return false;" href="#">START</a>
           <a onclick="offhook();
                   return false;" href="#">OFF HOOK</a>
           <a onclick="onhook();
                   return false;" href="#">ON HOOK</a>
           <a onclick="end();
                   return false;" href="#">END</a>

Target number: <input type="text" id="number" value="11" onchange="startPresence();"/>

Local Video<input type="checkbox" id="usevideo" checked="checked"/><video id="video-local" muted="muted">video</video>Remote Video<video id="video-remote" muted="muted">video</video>
Sharing: <input type="checkbox" id="usesharing" checked="checked"/>

   </body>

</html>