boostlingo-js

The boostlingo-js javascript library enables web application developers to embed the Boostlingo caller directly into their own site. This can then be used for placing video or voice calls in the Boostlingo platform.

Getting Started

In order to place calls in Boostlingo, you must have a requestor account. You can then embed boostlingo-js into your front end web application, request a Boostlingo API token from your server, and start making calls.

Installation

Include the boostlingo javascript client in your web app's html. Releases are published and hosted so you can include them directly by using a <script>.

<script src="https://connect.boostlingo.com/sdk/boostlingo-js/3.0.0/dist/loglevel.min.js" type="text/javascript"></script>
<script src="https://connect.boostlingo.com/sdk/boostlingo-js/3.0.0/dist/boostlingo.min.js" type="text/javascript"></script>

Alternate versions of the library are also created that do not bundle @twilio/voice-sdk and twilio-video. We do not recommend this approach since we cannot guarantee version compatibility, but if you follow our guidelines on which twilio libs to include it will absolutely work. Please reach out to us to confirm twilio library versions, also please notice the Twilio Voice SDK is no longer hosted via CDN, you will need to host it on your server if you wish to follow this approach.

<script src="https://YOUR_SERVER.com/twilio-sdk/2.12.4/twilio-voice.min.js" type="text/javascript"></script>
<script src="https://media.twiliocdn.com/sdk/js/video/releases/2.0.0/twilio-video.min.js" type="text/javascript"></script>
<script src="https://connect.boostlingo.com/sdk/boostlingo-js/3.0.0/dist/loglevel.min.js" type="text/javascript"></script>
<script src="https://connect.boostlingo.com/sdk/boostlingo-js/3.0.0/dist/boostlingo-without-twilio.min.js" type="text/javascript"></script>

Usage

These steps will guide you through the basic process of placing calls through Boostlingo

Request Boostlingo authentication token

First step is to obtain a boostlingo authentication token from your server. Never store an API token or username/password in your front end code. Your server should be the one that logs the user in, obtains the authentication token, and passes it back down to the web application.

A typical way to do this using jQuery is with an authenticated/secure GET request to your server. Please note, you must use a requestor account credentials to obtain a proper token for placing calls.

var blJS;

$.getJSON("https://YOUR_SERVER.com/get-boostlingo-token")
    .then(data => {
        console.log("Using Boostlingo Token: " + data.authToken);

        // Create an instance of the boostlingo library
        blJS = new BoostLingo(data.authToken);
    });

Obtain Boostlingo authentication token via API endpoint

POST https://app.boostlingo.com/api/web/account/signin

Request Model

{
    "email": "<string>",
    "password": "<string>"
}

Response Model token is what will be needed by the boostlingo-js library

{
    "userAccountId": "<integer>",
    "role": "<string>",
    "token": "<string>",
    "companyAccountId": "<integer>"
}

Create instance of Boostlingo class and load dictionaries

We recommend you do this only once. The Boostlingo library will cache specific data and create instances of classes that do not need to be refreshed very frequently. Expanding on the example above, the next step is typically to pull down the call dictionaries. Whether you expose these directly or are just mapping languages and service types with your internal types, loading these lists will almost definitely be required. In this example we populate a series of select dropdown inputs.

var blJS;

$.getJSON("https://YOUR_SERVER.com/get-boostlingo-token")
    .then(data => {
        console.log("Using Boostlingo Token: " + data.authToken);

        // Create an instance of the boostlingo library
        blJS = new BoostLingo(data.authToken);

        return blJS.getCallDictionaries();
    })
    .then(dict => {
        console.log("Successfully retrieved dictionaries", dict)

        // populate dropdowns with language pairs and service types
        var langFrom = $("#select-lang-from");
        var langTo = $("#select-lang-to");
        dict.languages.forEach(item => {
            var displayName = item.name + (item.isSignLanguage ? " (video only)" : "");
            langFrom.append($("<option>", { value: item.id, text: displayName }));
            langTo.append($("<option>", { value: item.id, text: displayName }));
        });

        var serviceType = $("#select-service-type");
        dict.serviceTypes.forEach(item => {
            serviceType.append($("<option>", { value: item.id, text: item.name }));
        });

        // all loaded, show controls
        $("#call-controls").fadeIn();
    })
    .catch(error => console.log("failure loading library", error));

// Handle "Call" button click
$("#button-call").on("click", function () {   
        // Optionally provide an array of objects to send key/value pairs with additional metadata  
        let callData = [];
        
        //previously populated Map with key/value pairs
        data.forEach((value, key) => {
            callData.push({ key, value });
        });
        
        // Pull call request details from the drop downs we previously populated.
        var callReq = {
            languageFromId: parseInt($("#select-lang-from").val()),
            languageToId: parseInt($("#select-lang-to").val()),
            serviceTypeId: parseInt($("#select-service-type").val()),
            genderId: parseInt($("#select-gender").val()),            
            data: callData.length > 0 ? callData : null
        };

    blJS.makeVoiceCall(callReq))
        .then(call => {
            // event that let's you know when remote participant has joined call
            call.on("callConnected", () => {
                console.log("Interpreter joined call");

                // UI should show call as active
            });

            // event fires when call is completed or cancelled 
            call.on("callCompleted", cancelled => {
                console.log("Call completed " + (cancelled ? "before": "after") + " connect");

                // reset UI back to inactive state
            });

            // event when the remote participant information is available
            call.on("interlocutorInfo", info => {
                console.log("Name:" + info.requiredName + " Company:" + info.companyName + " Rating:" + info.rating);

                // display whatever interpreter information you desire
            });
        })
        .catch(error => console.log("Error making call", error));
});

Example

This is a working example that will demonstrate how to place voice and video calls.

html

index.html

<!doctype html>
<html>
    <head lang='en'>
        <meta charset='utf-8'>
        <meta name='viewport' content='width=device-width'>
        <title>Boostlingo-js Test Page</title>

        <style>
            body {
                font-family: 'Helvetica Neue', Arial, sans-serif;
                color: #333;
                font-weight: 300;
            }

            div {
                margin-top: 10px;
            }

            div video {
                border: 1px solid #000000;
                background-color: #000000;
            }

            div#local-media video {
                height: 100px;
            }

            textarea#log {
                width: 50%;
                height: 200px;
            }

            table, th, td {
                border: 1px solid;
                padding: 10px;
            }
            
            table {
                border-collapse: collapse;
            }

            .attach-container {              
                border-style: solid;
                border-width: 2px;
                width: 50%;
            }

            #auto-attach {
                background-color: lightcoral;
            }
            #manual-attach {
                background-color: lightblue;
            }

            .bl-participant {
                height: 102px;
                width: 102px;
                border-style: solid;
                border-width: 2px;
            }

            div.bl-participant video {
                height: 100px;
                width: 100px;                
            } 

            div.bl-participant--remote {               
                border-color: blueviolet;                
            }

            div.bl-participant--local {
               border-color: green;               
            }

            div.bl-participant--dial-in {
               /*This represents a call without video.  You could use this class to provide a background image*/ 
               background-color: cadetblue;
            }

            .hide {
                display: none;
            }
        </style>
    </head>
    <body>
        <h1>Boostlingo-js Test Page</h1>

        <!-- using hosted library
        -->
        <script src="https://connect.boostlingo.com/sdk/boostlingo-js/3.0.0/dist/loglevel.min.js" type="text/javascript"></script>
        <script src="https://connect.boostlingo.com/sdk/boostlingo-js/3.0.0/dist/boostlingo.min.js" type="text/javascript"></script>

        <!-- test code -->
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
        <script src="test.js" type="text/javascript"></script>

        <div id="server-auth">
            <div>
                <label for="select-region">Region:</label>
                <select id="select-region"></select>
            </div>

            <div>
                <label for="input-token">Token:</label>
                <input type="text" id="input-token" size="110"/>
            </div>

            <div>
                <button id="button-auth">Authenticate</button>
            </div>
        </div>

        <div id="call-controls">
            <div>
                <label for="select-lang-from">Language From:</label>
                <select id="select-lang-from"></select>
            </div>

            <div>
                <label for="select-lang-to">Language To:</label>
                <select id="select-lang-to"></select>
            </div>

            <div>
                <label for="select-service-type">Service Type:</label>
                <select id="select-service-type"></select>
            </div>

            <div>
                <label for="select-gender">Gender:</label>
                <select id="select-gender"></select>
            </div>


            <div>
                <label>Data (Key/Value pairs):</label>
                <label for="key">Key:<input id="key" type="text" name="key"></label>
                <label for="value">Value:<input id="value" type="text" name="value"></label>
                <button id="button-add-pair">Add Pair</button>
                <button id="button-reset-pairs">Reset Pairs</button>
            </div>

            <div id="table-key-value">
                <!--Placeholder to display key/value pairs-->
            </div>


            <div>
                <label><input type="radio" name="commType" value="voice" checked="checked"> Voice</label>
                <label><input type="radio" name="commType" value="video"> Video</label>
            </div>

            <div>
                <button id="button-call">Call</button>
                <button id="button-hangup">Hangup</button>
                <button id="button-mute">Mute audio</button>
                <button id="button-unmute">Unmute audio</button>
                <button id="button-pause">Pause video</button>
                <button id="button-unpause">Unpause video</button>
            </div>

            <div id="video-only">        
                <div>
                    <span>Attach video containers to the DOM</span>
                    <label><input type="radio" name="attachType" value="auto" checked="checked">Automatically</label>
                    <label><input type="radio" name="attachType" value="manual">Manually</label>
                </div>
                <div id="auto-attach" class="attach-container">
                    <span>Video containers attached to the DOM automatically</span>
                </div>
                <div id="manual-attach" class="attach-container">
                    <span>Video containers attached to the DOM manually</span>
                </div>
            </div>
         
        </div>

        <div>
            <textarea id="log"></textarea>            
        </div>                   
        
    </body>
</html>

Javascript

test.js

$(function () {

    log("Using Boostlingo library version", BoostLingo.version());

    let data = new Map();

    var regionSelect = $("#select-region");
    BoostLingo.regions().forEach(item => {
        regionSelect.append($("<option>", { value: item, text: item }));
    });

    var blJS;
    var currentCall;

    // initially hide all call controls
    $("#call-controls").hide();
    resetCallButtons();

    $("#button-auth").on("click", function () {
        log("Authenticate...");
        var region = $("#select-region").val();
        console.log("region", region);

        var token = $("#input-token").val();

        console.log("BoostLingo test using Token: " + token);
        log("Loading BoostLingo dictionaries from", region);

        blJS = new BoostLingo(token, { region: region, logLevel: "debug" });

        blJS.getCallDictionaries()
            .then(dict => {
                log("Successfully retrieved dictionaries", dict)

                setupCallSettings();

                var serviceType = $("#select-service-type");
                dict.serviceTypes.forEach(item => {
                    serviceType.append($("<option>", { value: item.id, text: item.name }));
                });

                var gender = $("#select-gender");
                dict.genders.forEach(item => {
                    gender.append($("<option>", { value: item.id, text: item.name }));
                });

                // all loaded, show controls
                $("#server-auth").hide();
                $("#call-controls").fadeIn();
            })
            .catch(error => log("failed to get dictionaries, please check auth token", error));
    });

    $("#button-call").on("click", async function () {              
                
        // Optionally provide an array of objects to send key/value pairs with additional metadata  
        let callData = [];
        
        //previously populated Map with key/value pairs
        data.forEach((value, key) => {
            callData.push({ key, value });
        });        
        
        var callReq = {
            languageFromId: parseInt($("#select-lang-from").val()),
            languageToId: parseInt($("#select-lang-to").val()),
            serviceTypeId: parseInt($("#select-service-type").val()),
            genderId: parseInt($("#select-gender").val()),
            data: callData.length > 0 ? callData : null
        };

        const isVideo = document.querySelector('input[name=commType]:checked').value === "video";        
        log("Placing " + (isVideo ? "video" : "voice") + " call", callReq);

        let call;

        try {        
            if (isVideo) {
                //Make Video Call
                const autoAttach = document.querySelector('input[name=attachType]:checked').value === "auto";
                if (autoAttach) {
                    let videoContainer = document.getElementById("auto-attach");
                    //Automatically attach video elements to the DOM
                    call = await blJS.makeVideoCall(callReq, videoContainer);
                } else {
                    //The consumer of the SDK will have to manually add the video elements to the DOM. 
                    call = await blJS.makeVideoCall(callReq);
                }
            }

            if (!isVideo) {
                //Make Voice Call
                call = await blJS.makeVoiceCall(callReq);
            }

            log("Successfully made call", call);

            currentCall = call;
            $("#button-call").hide();
            $("#button-hangup").show();

            // event that let's you know when remote participant has joined call
            call.on("callConnected", () => log("Interpreter joined call"));

            // event fires when call is completed or cancelled 
            call.on("callCompleted", cancelled => {
                log("Call completed " + (cancelled ? "before": "after") + " connect");
                currentCall = null;
                resetCallButtons();
            });

            // event when the remote participant information is available
            // once available, can also be obtained with call.getInterlocutorInfo())
            call.on("interlocutorInfo", info => {
                log("Interpreter info available", info);
                log("Name:" + info.requiredName + " Company:" + info.companyName + " Rating:" + info.rating);
            });

            // events for when local and remote audio is shared and/or attached to DOM
            call.on("localAudioShared", isEnabled => {
                log("Started sharing local audio", isEnabled);
                // should just be one button, but showing both for testing purposes
                $("#button-mute").fadeIn();
                $("#button-unmute").fadeIn();
            });

            call.on("remoteAudioShared", isEnabled => log("Started sharing remote audio", isEnabled));

            // events for when local and remote audio is unshared and/or removed from DOM
            call.on("localAudioUnshared", () => log("Local audio unshared"));
            call.on("remoteAudioUnshared", () => log("Remote audio unshared"));

            // events for when local and remote audio is enabled (unmuted)
            // does not fire initially if shared in isEnabled state
            call.on("localAudioEnabled", () => log("Local audio enabled"));  // could toggle mute/unmute
            call.on("remoteAudioEnabled", () => log("Remote audio enabled"));

            // events for when local and remote audio is disabled (muted)
            call.on("localAudioDisabled", () => log("Local audio disabled"));
            call.on("remoteAudioDisabled", () => log("Remote audio disabled"));

            /*
                * Video specific events
                * wouldn't hurt to listen for voice calls, but why bother
                */
            if (call.isVideo()) {
                // events for when local and remote video is shared and/or attached to DOM
                call.on("localVideoShared", isEnabled => {
                    log("Started sharing local video", isEnabled);
                    $("#button-pause").fadeIn();
                    $("#button-unpause").fadeIn();
                });
                call.on("remoteVideoShared", isEnabled => log("Started sharing remote video", isEnabled));

                // events for when local and remote video is unshared and/or removed from DOM
                call.on("localVideoUnshared", () => log("Local video unshared"));
                call.on("remoteVideoUnshared", () => log("Remote video unshared"));

                // events for when local and remote video is enabled (unmuted)
                // does not fire initially if shared in isEnabled state
                call.on("localVideoEnabled", () => log("Local video enabled"));
                call.on("remoteVideoEnabled", () => log("Remote video enabled"));

                // events for when local and remote video is disabled (muted)
                call.on("localVideoDisabled", () => log("Local video disabled"));
                call.on("remoteVideoDisabled", () => log("Remote video disabled"));

                /*
                If a container was not provided when making a video call, the participants would not be attached to the DOM
                automatically. Instead, we need to react to the following events for adding or removing participants

                These events are only raised if a container is not provided during the video call.     
                */                
                call.on("remoteParticipantAttachContainer", div => {                     
                    $("#manual-attach")[0].appendChild(div);
                    log("container for remote participant attached")
                });

                call.on("remoteParticipantDetachContainer", div => {                     
                    let divToRemove  = document.getElementById(div.id);
                    divToRemove.remove();
                    log("container for remote participant detached")
                });

                call.on("localParticipantAttachContainer", div => {                     
                    $("#manual-attach")[0].appendChild(div);
                    log("container for local participant attached")
                });

                call.on("localParticipantDetachContainer", div => {                   
                    div.remove();
                    log("container for local participant detached")
                });
            }
        } catch (error) {
            log("Error making call", error);
        }        
    });

    $("#button-hangup").on("click", function () {
        log("Hanging up...");
        if (blJS) {
            // current call can also be obtained with getCurrentCall()
            var call = blJS.getCurrentCall();
            if (call) {
                var lastCallId = call._callInfo.callId;
                call.hangup() // wait on hangup so we can test getting call details
                    .then(() => blJS.getCallDetails(lastCallId))
                    .then(d => log("Call Details acquired", d))
                    .catch(e => log("Failed to get call details", e));
            }
            else
                log("No call to hangup");
        }
    });

    $("#button-mute").on("click", function () {
        log("Mute...");
        if (currentCall) {
            currentCall.disableAudio();
        }
    });

    $("#button-unmute").on("click", function () {
        log("Unmute...");
        if (currentCall) {
            currentCall.enableAudio();
        }
    });

    $("#button-pause").on("click", function () {
        log("Pause...");
        if (currentCall) {
            currentCall.disableVideo();
        }
    });

    $("#button-unpause").on("click", function () {
        log("Unpause...");
        if (currentCall) {
            currentCall.enableVideo();
        }
    });

    $("input[name=commType]").change(setupCallSettings);

    $("#button-add-pair").on("click", function () {                        
        const key = document.getElementById('key').value;
        const value = document.getElementById('value').value;              
        
        if(key === '' || value === '') {
            
            log('Both values are required for the Key/Value pair. The pair was not added.')
            return;
        }

        log(`Key/Value pair added: (${key}/${value})`);        
        data.set(key,value);       
        renderKeyValuePairs();
        resetInputPairs();
    });

    $("#button-reset-pairs").on("click", function () {                
        console.log(data);
        data = new Map();
        renderKeyValuePairs();
        log('All Key/Value pairs have been removed');               

    });

    function setupCallSettings() {        
        const callType = document.querySelector('input[name=commType]:checked').value;
        const videoResult = document.getElementById("video-only");

        if( callType === "voice") {
            videoResult.classList.add("hide");
        } else{
            videoResult.classList.remove("hide");
        }
        setupLanguageLists();
    }

    function setupLanguageLists() {
        if (!blJS)
            return;

        var commType = $("input[name=commType]:checked").val();
        log("Setting up language lists for", commType);
        (commType === "video" ? blJS.getVideoLanguages() : blJS.getVoiceLanguages())
            .then(langs => { 
                var langFrom = $("#select-lang-from");
                var langTo = $("#select-lang-to");

                // First empty lists
                langFrom.empty();
                langTo.empty();

                // Then repopulate with current list
                langs.forEach(item => {
                    var displayName = item.name + (item.isSignLanguage ? " (sign language)" : "");
                    langFrom.append($("<option>", { value: item.id, text: displayName }));
                    langTo.append($("<option>", { value: item.id, text: displayName }));
                });
            });
    }

    function resetCallButtons() {
        $("#button-call").show();
        $("#button-hangup").hide();
        $("#button-mute").hide();
        $("#button-unmute").hide();
        $("#button-pause").hide();
        $("#button-unpause").hide();
    }

    function log(message, data) {
        if (data != null)
            console.log(message, data);
        else
            console.log(message);

        if (typeof data === "string")
            message += ": " + data;

        var logArea = $("#log");
        logArea.append("> " + message + "\n");
        logArea.scrollTop(logArea[0].scrollHeight - logArea.height());
    }

    function resetInputPairs() {
        //get elements
        const keyElement = document.getElementById('key');
        const valueElement = document.getElementById('value');              

        //reset controls
        keyElement.value = '';
        valueElement.value = '';      
    }

    function renderKeyValuePairs() {       
        let pairs = '';     

        for (let [key, value] of  data.entries()) {
            pairs += ` <tr>
                        <td>${key}</td>
                        <td>${value}</td>
                      </tr>`
        }
        const table = `
            <table>
                <tr>
                    <th>Key</th>
                    <th>Value</th>        
                </tr>
                ${pairs}
            </table>`

        const keyValueTable = document.getElementById('table-key-value');
        keyValueTable.innerHTML = '';
        keyValueTable.insertAdjacentHTML('afterbegin', table);       
    }
});

Example - interpreter

This is a working example that will demonstrate how to receive voice and video calls.

html

test-interpreter.html

<!doctype html>
<html>
    <head lang='en'>
        <meta charset='utf-8'>
        <meta name='viewport' content='width=device-width'>
        <title>Boostlingo-js Interpreter Test Page</title>

        <style>
            body {
                font-family: 'Helvetica Neue', Arial, sans-serif;
                color: #333;
                font-weight: 300;
            }

            div {
                margin-top: 10px;
            }

            div video {
                border: 1px solid #000000;
                background-color: #000000;
            }

            div#local-media video {
                height: 100px;
            }

            textarea#log {
                width: 50%;
                height: 200px;
            }
        </style>
    </head>
    <body>
        <h1>Boostlingo-js Interpreter Test Page</h1>

        <!-- use hosted library -->
        <script src="https://connect.boostlingo.com/sdk/boostlingo-js/3.0.0/dist/loglevel.min.js" type="text/javascript"></script>
        <script src="https://connect.boostlingo.com/sdk/boostlingo-js/3.0.0/dist/boostlingo.min.js" type="text/javascript"></script>

        <!-- test code -->
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
        <script src="test-interpreter.js" type="text/javascript"></script>

        <div id="server-auth">
            <div>
                <label for="select-region">Region:</label>
                <select id="select-region"></select>
            </div>

            <div>
                <label for="input-token">Token:</label>
                <input type="text" id="input-token" size="110" value=""/>
            </div>

            <div>
                <button id="button-auth">Authenticate</button>
            </div>
        </div>

        <div id="call-controls">
            <div>
                <button id="button-start">Start connection</button>
                <button id="button-listeners">Register listeners</button>
                <button id="button-online">Go online</button>
                <button id="button-offline">Go offline</button>
                <button id="button-pick">Pick call</button>
                <button id="button-hangup">Hangup</button>
                <button id="button-mute">Mute audio</button>
                <button id="button-unmute">Unmute audio</button>
                <button id="button-pause">Pause video</button>
                <button id="button-unpause">Unpause video</button>
            </div>

            <div id="remote-media"></div>
            <div id="local-media"></div>
        </div>

        <div>
            <textarea id="log"></textarea>
        </div>

    </body>
</html>

Javascript

test-interpreter.js

$(function () {

    log("Using BoostLingo library version", BoostLingo.version());

    var regionSelect = $("#select-region");
    BoostLingo.regions().forEach(item => {
        regionSelect.append($("<option>", { value: item, text: item }));
    });

    var blJS;

    // initially hide all call controls
    $("#call-controls").hide();
    resetCallButtons();

    $("#button-auth").on("click", function () {
        log("Authenticate...");
        var region = $("#select-region").val();
        console.log("region", region);

        var token = $("#input-token").val();

        console.log("BoostLingo test using Token: " + token);

        blJS = new BoostLingo(token, { region: region, logLevel: "debug" });

        blJS.on("incomingCallNew", function (callInfo) {
            log(`Incoming ${callInfo.IsVideo ? 'Video' : 'Audio'} Call: ID: ${callInfo.CallId}, account unique ID: ${callInfo.AccountUniqueId}, ${callInfo.LanguageFromName} > ${callInfo.LanguageToName}, ${callInfo.ServiceTypeName}`);
        });

        blJS.on("incomingCallLost", function (callInfo) {
            log(`Incoming ${callInfo.IsVideo ? 'Video' : 'Audio'} Call Lost: ID: ${callInfo.CallId}, account unique ID: ${callInfo.AccountUniqueId}, ${callInfo.LanguageFromName} > ${callInfo.LanguageToName}, ${callInfo.ServiceTypeName}`);
        });

        blJS.on("incomingCallAccepted", function (callInfo) {
            log(`Incoming ${callInfo.IsVideo ? 'Video' : 'Audio'} Call Accepted: ID: ${callInfo.CallId}, account unique ID: ${callInfo.AccountUniqueId}, ${callInfo.LanguageFromName} > ${callInfo.LanguageToName}, ${callInfo.ServiceTypeName}`);
        });

        blJS.on("incomingCallConnected", function (callInfo) {
            log(`Incoming ${callInfo.IsVideo ? 'Video' : 'Audio'} Call Connected: ID: ${callInfo.CallId}, account unique ID: ${callInfo.AccountUniqueId}, ${callInfo.LanguageFromName} > ${callInfo.LanguageToName}, ${callInfo.ServiceTypeName}`);
        });

        blJS.on("incomingCallTakenByOther", function (callInfo) {
            log(`Incoming ${callInfo.IsVideo ? 'Video' : 'Audio'} Call Taken by Other: ID: ${callInfo.CallId}, account unique ID: ${callInfo.AccountUniqueId}, ${callInfo.LanguageFromName} > ${callInfo.LanguageToName}, ${callInfo.ServiceTypeName}`);
        });

        blJS.on("incomingCallRefused", function (callInfo) {
            log(`Incoming ${callInfo.IsVideo ? 'Video' : 'Audio'} Call Refused: ID: ${callInfo.CallId}, account unique ID: ${callInfo.AccountUniqueId}, ${callInfo.LanguageFromName} > ${callInfo.LanguageToName}, ${callInfo.ServiceTypeName}`);
        });

        blJS.on("incomingCallFailedToConnect", function (callInfo) {
            log(`Incoming ${callInfo.IsVideo ? 'Video' : 'Audio'} Call Failed to Connect: ID: ${callInfo.CallId}, account unique ID: ${callInfo.AccountUniqueId}, ${callInfo.LanguageFromName} > ${callInfo.LanguageToName}, ${callInfo.ServiceTypeName}`);
        });

        blJS.on("incomingCallCompletedSuccessfully", function (callInfo) {
            log(`Incoming ${callInfo.IsVideo ? 'Video' : 'Audio'} Call Completed Successfully: ID: ${callInfo.CallId}, account unique ID: ${callInfo.AccountUniqueId}, ${callInfo.LanguageFromName} > ${callInfo.LanguageToName}, ${callInfo.ServiceTypeName}`);
        });

        blJS.on("incomingCallCompletedWithIssues", function (callInfo) {
            log(`Incoming ${callInfo.IsVideo ? 'Video' : 'Audio'} Call Completed with Issues: ID: ${callInfo.CallId}, account unique ID: ${callInfo.AccountUniqueId}, ${callInfo.LanguageFromName} > ${callInfo.LanguageToName}, ${callInfo.ServiceTypeName}`);
        });

        // Show controls
        $("#server-auth").hide();
        $("#call-controls").fadeIn();
    });

    $("#button-start").on("click", function () {
        log("Starting interpreter SignalR connection...");
        if (blJS) {
            return blJS.startInterpreterSignalRConnection()
                .then(startSignalRConnectionResponse => {
                    log("Started interpreter SignalR connection");
                })
                .catch(error => log("Error starting interpreter SignalR connection", error));
        }
    });

    $("#button-listeners").on("click", function () {
        log("Registering interpreter SignalR listeners...");
        if (blJS) {
            blJS.registerInterpreterSignalRListeners("local-media", "remote-media")
                .then(registerInterpreterSignalRListenersResponse => {
                    log("Registered interpreter SignalR listeners");
                })
                .catch(error => log("Error registering interpreter SignalR listeners", error));
        }
    });

    $("#button-online").on("click", function () {
        log("Going online...");
        if (blJS) {
            blJS.goOnline(null, null)
                .then(goOnlineResponse => {
                    log(`Went online, SignalR connection Id: ${goOnlineResponse.ConnectionId}`)
                })
                .catch(error => log("Error going online", error));

        }
    });

    $("#button-offline").on("click", function () {
        log("Going offline...");
        if (blJS) {
            blJS.goOffline()
                .then(goOfflineResponse => {
                    log("Went offline")
                })
                .catch(error => log("Error going offline", error));
        }
    });

    $("#button-pick").on("click", function () {
        log("Picking call...");
        if (blJS) {
            blJS.pickCall()
                .then(pickCallResponse => {
                    log("Picked call");
                })
                .catch(error => log("Error picking call", error));
        }
    });

    $("#button-hangup").on("click", function () {
        log("Hanging up call...");
        if (blJS) {
            blJS.hangupCall()
                .then(hangupCallResponse => {
                    log("Hanged up call")
                })
                .catch(error => log("Error hanging up", error));
        }
    });

    $("#button-mute").on("click", function () {
        log("Muting...");
        blJS.mute()
            .then(muteResponse => {
                log(`Muted`);
            })
            .catch(error => log("Error muting", error));
    });

    $("#button-unmute").on("click", function () {
        log("Unmuting...");
        blJS.unmute()
            .then(unmuteResponse => {
                log(`Unmuted`)
            })
            .catch(error => log("Error unmuting", error));
    });

    $("#button-pause").on("click", function () {
        log("Disabling video...");
        if (blJS) {
            blJS.disableVideo()
                .then(disableVideoResponse => {
                    log(`Disabled video`);
                })
                .catch(error => log("Error disabling video", error));
        }
    });

    $("#button-unpause").on("click", function () {
        log("Enabling video...");
        if (blJS) {
            blJS.enableVideo()
                .then(enableVideoResponse => {
                    log(`Enabled video`);
                })
                .catch(error => log("Error enabling video", error));
        }
    });

    function resetCallButtons() {
        $("#button-start").show();
        $("#button-listeners").show();
        $("#button-online").show();
        $("#button-offline").show();
        $("#button-pick").show();
        $("#button-hangup").show();
        $("#button-mute").show();
        $("#button-unmute").show();
        $("#button-pause").show();
        $("#button-unpause").show();
    }

    function log(message, data) {
        if (data != null)
            console.log(message, data);
        else
            console.log(message);

        if (typeof data === "string")
            message += ": " + data;

        var logArea = $("#log");
        logArea.append("> " + message + "\n");
        logArea.scrollTop(logArea[0].scrollHeight - logArea.height());
    }
});