// We import the settings.js file to know which address we should contact // to talk to Janus, and optionally which STUN/TURN servers should be // used as well. Specifically, that file defines the "server" and // "iceServers" properties we'll pass when creating the Janus session. /* global iceServers:readonly, Janus:readonly, server:readonly */ /* global md5:readonly */ var janus = null; var sipcall = null; var opaqueId = "siptest-" + Janus.randomString(12); var localTracks = {}, localVideos = 0, remoteTracks = {}, remoteVideos = 0; var selectedApproach = null; var registered = false; var masterId = null, helpers = {}, helpersCount = 0; var incoming = null; $(document).ready(function () { // Initialize the library (all console debuggers enabled) Janus.init({ debug: "all", callback: function () { // Use a button to start the demo $("#start").one("click", function () { $(this).attr("disabled", true).unbind("click"); // Make sure the browser supports WebRTC if (!Janus.isWebrtcSupported()) { bootbox.alert("No WebRTC support... "); return; } // Create session janus = new Janus({ server: server, iceServers: iceServers, // Should the Janus API require authentication, you can specify either the API secret or user token here too // token: "mytoken", // or // apisecret: "serversecret", success: function () { // Attach to SIP plugin janus.attach({ plugin: "janus.plugin.sip", opaqueId: opaqueId, success: function (pluginHandle) { $("#details").remove(); sipcall = pluginHandle; Janus.log( "Plugin attached! (" + sipcall.getPlugin() + ", id=" + sipcall.getId() + ")" ); // Prepare the username registration $("#sipcall").removeClass("hide"); $("#login").removeClass("invisible").removeClass("hide"); $("#registerlist a") .unbind("click") .click(function () { $(".dropdown-toggle").dropdown("hide"); selectedApproach = $(this).attr("id"); $("#registerset") .html($(this).html()) .parent() .removeClass("open"); if (selectedApproach === "guest") { $("#password").empty().attr("disabled", true); } else { $("#password").removeAttr("disabled"); } switch (selectedApproach) { case "secret": bootbox.alert( "Using this approach you'll provide a plain secret to REGISTER" ); break; case "ha1secret": bootbox.alert( "Using this approach might not work with Asterisk because the generated HA1 secret could have the wrong realm" ); break; case "guest": bootbox.alert( "Using this approach you'll try to REGISTER as a guest, that is without providing any secret" ); break; default: break; } return false; }); $("#register").click(registerUsername); $("#server").focus(); $("#start") .removeAttr("disabled") .html("Stop") .click(function () { $(this).attr("disabled", true); janus.destroy(); }); }, error: function (error) { Janus.error(" -- Error attaching plugin...", error); bootbox.alert(" -- Error attaching plugin... " + error); }, consentDialog: function (on) { Janus.debug( "Consent dialog should be " + (on ? "on" : "off") + " now" ); if (on) { // Darken screen and show hint $.blockUI({ message: '
', baseZ: 3001, css: { border: "none", padding: "15px", backgroundColor: "transparent", color: "#aaa", top: "10px", left: "100px", }, }); } else { // Restore screen $.unblockUI(); } }, iceState: function (state) { Janus.log("ICE state changed to " + state); }, mediaState: function (medium, on, mid) { Janus.log( "Janus " + (on ? "started" : "stopped") + " receiving our " + medium + " (mid=" + mid + ")" ); }, webrtcState: function (on) { Janus.log( "Janus says our WebRTC PeerConnection is " + (on ? "up" : "down") + " now" ); $("#videoleft").parent().unblock(); }, slowLink: function (uplink, lost, mid) { Janus.warn( "Janus reports problems " + (uplink ? "sending" : "receiving") + " packets on mid " + mid + " (" + lost + " lost packets)" ); }, onmessage: function (msg, jsep) { Janus.debug(" ::: Got a message :::", msg); // Any error? let error = msg["error"]; if (error) { if (!registered) { $("#server").removeAttr("disabled"); $("#username").removeAttr("disabled"); $("#authuser").removeAttr("disabled"); $("#displayname").removeAttr("disabled"); $("#password").removeAttr("disabled"); $("#register") .removeAttr("disabled") .click(registerUsername); $("#registerset").removeAttr("disabled"); } else { // Reset status sipcall.hangup(); $("#dovideo").removeAttr("disabled").val(""); $("#peer").removeAttr("disabled").val(""); $("#call") .removeAttr("disabled") .html("Call") .removeClass("btn-danger") .addClass("btn-success") .unbind("click") .click(doCall); } bootbox.alert(error); return; } let callId = msg["call_id"]; let result = msg["result"]; if (result && result["event"]) { let event = result["event"]; if (event === "registration_failed") { Janus.warn( "Registration failed: " + result["code"] + " " + result["reason"] ); $("#server").removeAttr("disabled"); $("#username").removeAttr("disabled"); $("#authuser").removeAttr("disabled"); $("#displayname").removeAttr("disabled"); $("#password").removeAttr("disabled"); $("#register") .removeAttr("disabled") .click(registerUsername); $("#registerset").removeAttr("disabled"); bootbox.alert(result["code"] + " " + result["reason"]); return; } if (event === "registered") { Janus.log( "Successfully registered as " + result["username"] + "!" ); $("#you") .removeClass("hide") .text("Registered as '" + result["username"] + "'"); // TODO Enable buttons to call now if (!registered) { registered = true; masterId = result["master_id"]; $("#server").parent().addClass("hide"); $("#authuser").parent().addClass("hide"); $("#displayname").parent().addClass("hide"); $("#password").parent().addClass("hide"); $("#register").parent().addClass("hide"); $("#registerset").parent().addClass("hide"); $("#addhelper").removeClass("hide").click(addHelper); $("#phone").removeClass("invisible").removeClass("hide"); $("#call").unbind("click").click(doCall); $("#peer").focus(); } } else if (event === "calling") { Janus.log("Waiting for the peer to answer..."); // TODO Any ringtone? $("#call") .removeAttr("disabled") .html("Hangup") .removeClass("btn-success") .addClass("btn-danger") .unbind("click") .click(doHangup); } else if (event === "incomingcall") { Janus.log("Incoming call from " + result["username"] + "!"); sipcall.callId = callId; let doAudio = true, doVideo = true; let offerlessInvite = false; if (jsep) { // What has been negotiated? doAudio = jsep.sdp.indexOf("m=audio ") > -1; doVideo = jsep.sdp.indexOf("m=video ") > -1; Janus.debug( "Audio " + (doAudio ? "has" : "has NOT") + " been negotiated" ); Janus.debug( "Video " + (doVideo ? "has" : "has NOT") + " been negotiated" ); } else { Janus.log( "This call doesn't contain an offer... we'll need to provide one ourselves" ); offerlessInvite = true; // In case you want to offer video when reacting to an offerless call, set this to true doVideo = false; } // Is this the result of a transfer? let transfer = ""; let referredBy = result["referred_by"]; if (referredBy) { transfer = " (referred by " + referredBy + ")"; transfer = transfer.replace(new RegExp("<", "g"), "<"); transfer = transfer.replace(new RegExp(">", "g"), ">"); } // Any security offered? A missing "srtp" attribute means plain RTP let rtpType = ""; let srtp = result["srtp"]; if (srtp === "sdes_optional") rtpType = " (SDES-SRTP offered)"; else if (srtp === "sdes_mandatory") rtpType = " (SDES-SRTP mandatory)"; // Notify user bootbox.hideAll(); let extra = ""; if (offerlessInvite) extra = " (no SDP offer provided)"; incoming = bootbox.dialog({ message: "Incoming call from " + result["username"] + "!" + transfer + rtpType + extra, title: "Incoming call", closeButton: false, buttons: { success: { label: "Answer", className: "btn-success", callback: function () { incoming = null; $("#peer") .val(result["username"]) .attr("disabled", true); // Notice that we can only answer if we got an offer: if this was // an offerless call, we'll need to create an offer ourselves let sipcallAction = offerlessInvite ? sipcall.createOffer : sipcall.createAnswer; // We want bidirectional audio and/or video let tracks = []; if (doAudio) tracks.push({ type: "audio", capture: true, recv: true, }); if (doVideo) tracks.push({ type: "video", capture: true, recv: true, }); sipcallAction({ jsep: jsep, tracks: tracks, success: function (jsep) { Janus.debug( "Got SDP " + jsep.type + "! audio=" + doAudio + ", video=" + doVideo + ":", jsep ); sipcall.doAudio = doAudio; sipcall.doVideo = doVideo; let body = { request: "accept" }; // Note: as with "call", you can add a "srtp" attribute to // negotiate/mandate SDES support for this incoming call. // The default behaviour is to automatically use it if // the caller negotiated it, but you may choose to require // SDES support by setting "srtp" to "sdes_mandatory", e.g.: // let body = { request: "accept", srtp: "sdes_mandatory" }; // This way you'll tell the plugin to accept the call, but ONLY // if SDES is available, and you don't want plain RTP. If it // is not available, you'll get an error (452) back. You can // also specify the SRTP profile to negotiate by setting the // "srtp_profile" property accordingly (the default if not // set in the request is "AES_CM_128_HMAC_SHA1_80") // Note 2: by default, the SIP plugin auto-answers incoming // re-INVITEs, without involving the browser/client: this is // for backwards compatibility with older Janus clients that // may not be able to handle them. Since we want to receive // re-INVITES to handle them ourselves, we specify it here: body["autoaccept_reinvites"] = false; sipcall.send({ message: body, jsep: jsep }); $("#call") .removeAttr("disabled") .html("Hangup") .removeClass("btn-success") .addClass("btn-danger") .unbind("click") .click(doHangup); }, error: function (error) { Janus.error("WebRTC error:", error); bootbox.alert( "WebRTC error... " + error.message ); // Don't keep the caller waiting any longer, but use a 480 instead of the default 486 to clarify the cause let body = { request: "decline", code: 480 }; sipcall.send({ message: body }); }, }); }, }, danger: { label: "Decline", className: "btn-danger", callback: function () { incoming = null; let body = { request: "decline" }; sipcall.send({ message: body }); }, }, }, }); } else if (event === "accepting") { // Response to an offerless INVITE, let's wait for an 'accepted' } else if (event === "progress") { Janus.log( "There's early media from " + result["username"] + ", wairing for the call!", jsep ); // Call can start already: handle the remote answer if (jsep) { sipcall.handleRemoteJsep({ jsep: jsep, error: doHangup }); } toastr.info("Early media..."); } else if (event === "accepted") { Janus.log(result["username"] + " accepted the call!", jsep); // Call can start, now: handle the remote answer if (jsep) { sipcall.handleRemoteJsep({ jsep: jsep, error: doHangup }); } toastr.success("Call accepted!"); sipcall.callId = callId; } else if (event === "updatingcall") { // We got a re-INVITE: while we may prompt the user (e.g., // to notify about media changes), to keep things simple // we just accept the update and send an answer right away Janus.log("Got re-INVITE"); let doAudio = jsep.sdp.indexOf("m=audio ") > -1, doVideo = jsep.sdp.indexOf("m=video ") > -1; // We want bidirectional audio and/or video, but only // populate tracks if we weren't sending something before let tracks = []; if (doAudio && !sipcall.doAudio) { sipcall.doAudio = true; tracks.push({ type: "audio", capture: true, recv: true }); } if (doVideo && !sipcall.doVideo) { sipcall.doVideo = true; tracks.push({ type: "video", capture: true, recv: true }); } sipcall.createAnswer({ jsep: jsep, tracks: tracks, success: function (jsep) { Janus.debug( "Got SDP " + jsep.type + "! audio=" + doAudio + ", video=" + doVideo + ":", jsep ); let body = { request: "update" }; sipcall.send({ message: body, jsep: jsep }); }, error: function (error) { Janus.error("WebRTC error:", error); bootbox.alert("WebRTC error... " + error.message); }, }); } else if (event === "message") { // We got a MESSAGE let sender = result["displayname"] ? result["displayname"] : result["sender"]; let content = result["content"]; content = content.replace(new RegExp("<", "g"), "<"); content = content.replace(new RegExp(">", "g"), ">"); toastr.success(content, "Message from " + sender); } else if (event === "info") { // We got an INFO let sender = result["displayname"] ? result["displayname"] : result["sender"]; let content = result["content"]; content = content.replace(new RegExp("<", "g"), "<"); content = content.replace(new RegExp(">", "g"), ">"); toastr.info(content, "Info from " + sender); } else if (event === "notify") { // We got a NOTIFY let notify = result["notify"]; let content = result["content"]; toastr.info(content, "Notify (" + notify + ")"); } else if (event === "transfer") { // We're being asked to transfer the call, ask the user what to do let referTo = result["refer_to"]; let referredBy = result["referred_by"] ? result["referred_by"] : "an unknown party"; let referId = result["refer_id"]; let replaces = result["replaces"]; let extra = "referred by " + referredBy; if (replaces) extra += ", replaces call-ID " + replaces; extra = extra.replace(new RegExp("<", "g"), "<"); extra = extra.replace(new RegExp(">", "g"), ">"); bootbox.confirm( "Transfer the call to " + referTo + "? (" + extra + ")", function (result) { if (result) { // Call the person we're being transferred to if (!sipcall.webrtcStuff.pc) { // Do it here $("#peer").val(referTo).attr("disabled", true); actuallyDoCall(sipcall, referTo, false, referId); } else { // We're in a call already, use a helper let h = -1; if (Object.keys(helpers).length > 0) { // See if any of the helpers if available for (let i in helpers) { if (!helpers[i].sipcall.webrtcStuff.pc) { h = parseInt(i); break; } } } if (h !== -1) { // Do in this helper $("#peer" + h) .val(referTo) .attr("disabled", true); actuallyDoCall( helpers[h].sipcall, referTo, false, referId ); } else { // Create a new helper addHelper(function (id) { // Do it here $("#peer" + id) .val(referTo) .attr("disabled", true); actuallyDoCall( helpers[id].sipcall, referTo, false, referId ); }); } } } else { // We're rejecting the transfer let body = { request: "decline", refer_id: referId }; sipcall.send({ message: body }); } } ); } else if (event === "hangup") { if (incoming != null) { incoming.modal("hide"); incoming = null; } Janus.log( "Call hung up (" + result["code"] + " " + result["reason"] + ")!" ); bootbox.alert(result["code"] + " " + result["reason"]); // Reset status sipcall.hangup(); $("#dovideo").removeAttr("disabled").val(""); $("#peer").removeAttr("disabled").val(""); $("#call") .removeAttr("disabled") .html("Call") .removeClass("btn-danger") .addClass("btn-success") .unbind("click") .click(doCall); } else if (event === "messagedelivery") { // message delivery status let reason = result["reason"]; let code = result["code"]; let callid = msg["call_id"]; if (code == 200) { toastr.success( `${callid} Delivery Status: ${code} ${reason}` ); } else { toastr.error( `${callid} Delivery Status: ${code} ${reason}` ); } } } }, onlocaltrack: function (track, on) { Janus.debug( "Local track " + (on ? "added" : "removed") + ":", track ); // We use the track ID as name of the element, but it may contain invalid characters let trackId = track.id.replace(/[{}]/g, ""); if (!on) { // Track removed, get rid of the stream and the rendering let stream = localTracks[trackId]; if (stream) { try { let tracks = stream.getTracks(); for (let i in tracks) { let mst = tracks[i]; if (mst) mst.stop(); } } catch (e) {} } if (track.kind === "video") { $("#myvideot" + trackId).remove(); localVideos--; if (localVideos === 0) { // No video, at least for now: show a placeholder if ($("#videoleft .no-video-container").length === 0) { $("#videoleft").append( '
' + '' + 'No webcam available' + "
" ); } } } delete localTracks[trackId]; return; } // If we're here, a new track was added let stream = localTracks[trackId]; if (stream) { // We've been here already return; } if ($("#videoleft video").length === 0) { $("#videos").removeClass("hide"); } if (track.kind === "audio") { // We ignore local audio tracks, they'd generate echo anyway if (localVideos === 0) { // No video, at least for now: show a placeholder if ($("#videoleft .no-video-container").length === 0) { $("#videoleft").append( '
' + '' + 'No webcam available' + "
" ); } } } else { // New video track: create a stream out of it localVideos++; $("#videoleft .no-video-container").remove(); stream = new MediaStream([track]); localTracks[trackId] = stream; Janus.log("Created local stream:", stream); $("#videoleft").append( '