How to abstract the complications of SIP.js away with our library
Written by Ard Timmerman on 17th October 2019
For VoIPGRID, our telephony platform, we have been working on a groundbreaking product that enables VoIP calling through the browser. The Webphone has been using SIP.js under-the-hood to make this possible. It was a big help because it handles a lot of complex stuff that is required in order to make a phone call. Think of interpreting all WebSocket communication, managing the WebRTC peer connection, etcetera. Although using SIP.js in our Webphone worked for our team at the time, using it gets complicated quickly. That’s why we decided to come up with a solution to make developing the Webphone faster.
The complexities of SIP.js
In order to develop on our Webphone, you need to learn a lot about how SIP.js works. This makes working on the Webphone really complicated, which in turn limits the number of developers that can work on it.
Four months ago the Vialer team started thinking about how we could make our lives easier (as developers often do), and we came up with the idea of building a library that is responsible for simplifying the complexities of handling calls with SIP.js – in other words, abstraction.
Where the problem lies
Setting up a call using SIP.js is straightforward. The complication is introduced when you try to use SIP.js for multiple things at once. There is a massive amount of events that SIP.js triggers, and you have to know them and how they impact one another before you are able to create a stable application.
For example, below I put some ‘basic’ code where I use SIP.js to place an outgoing call, receive an incoming call, and be subscribed to the status of a colleague while also responding to failures, errors, and some edge cases.
import { UA } from 'sip.js'; var userAgent = new UA({ uri: "bob@example.onsip.com", transportOptions: { wsServers: ["wss://sip-ws.example.com"], iceServers: [] }, authorizationUser: "", password: "" }); let isRegistering = false; let isRegistered = false; function tryToRegister() { // Do nothing if we are already registering. if(isRegistering) { return; } isRegistering = true; userAgent.register(); } let subscription = undefined; let isTransportConnecting = false; let isTransportConnected = false; function tryToConnectTransport() { userAgent.once("transportCreated", () => { if (isTransportConnecting) { return; } // Now that the transport is created, we can listen to an event which // tells us when we are connected to the sip server. userAgent.transport.once("connected", tryToRegister); userAgent.transport.once("disconnected", () => { tryToConnectTransport(); // If we had a subscription before, make sure to remove its // eventListeners so they won't trigger another time. if (subscription) { subscription.removeAllEventListers(); } }); }); } userAgent.once("registrationFailed", () => { isRegistering = false; tryToRegister(); }); function setupSessionListeners(session) { // Maybe the destination does not exist. session.once("failed", showFailed); // When the call is accepted by the sip server and in progress. session.once("progress", showInProgress); session.once("accepted", showAccepted); // When the target rejects the call. session.once("rejected", showRejected); // When one of the parties hangs up. session.once("terminated", showTerminated); } function setupOutgoingCallIfPossible() { if (!isRegistered) { console.log('Please register first.'); } const session = userAgent.invite("phoneNumber"); setupSessionListeners(session) } const onNotify = function(message) { const status = parseStatus(message); console.log(`Fred's status: ${status}`); } const fredsUri = "fred@voipgrid.nl"; function subscribeToFred() { // To avoid having multiple listeners active. if (subscription) { subscription.removeListener("notify", onNotify); } subscription = userAgent.transport.subscribe(uri); subscription.on("notify", onNotify); // It could be that we are rate-limited, in that case, parse the // retry-after header and figure out how long we should wait // before subscribing again. subscription.once("failed", (response, cause) => { const retryAfter = response.getHeader("Retry-After"); setTimeout(subscribeToFred, Number(retryAfter) * 1000); } } userAgent.on("registered", () => { isRegistered = true; callButton.on('clicked', setupOutgoingCallIfPossible); subscribeToFred(); }); userAgent.on("invite", setupSessionListeners); userAgent.start();
As you can see, there are many places where checks are needed to determine whether specific functionality can be used. You’ll notice that if you read the code, you would have to jump around to figure out what is going on. Now imagine this woven into an application that has to do a lot more than just manage calls, and you’ve got yourself a hard-to-maintain product.
Where the abstraction happens
This is where our library comes in. It wraps around SIP.js and hides away the complexities that make developing miserable.
With our library we aim to provide an easy-to-use wrapper, where we leverage asynchronous workflows to do your bidding. Below I’ve added a sample of code which covers the same functionalities as the first example.
const account = { user: "", password: "" }; const transport = { wsSServers: "wss://sip-ws.example.com" }; const media = { input: { id: undefined, audioProcessing: true, volume: 1.0, muted: false }, output: { id: undefined, volume: 1.0, muted: false } }; const client = new Client({ account, transport, media }); // incoming call client.on("invite", async session => { session.accept(); // auto-answer if (await session.accepted()) { console.log("Your call is accepted"); await session.terminated(); } else { console.log("Your call is rejected... =("); } }); await client.connect(); const fredsUri = "fred@voipgrid.nl"; client.on("notify", (uri, status) => { if (uri === fredsUri) { console.log(`Fred's status: ${status}`); } }); await client.subscribe(fredsUri); // outgoing call callButton.on("clicked", async () => { await client.invite("phoneNumber"); if (await session.accepted()) { console.log("Your call is accepted"); await session.terminated(); } else { console.log("Your call is rejected... =("); } });
Well actually, it does more than the first example. It switches over to a new WebSocket when you switch network interfaces during a call, it revives dead subscriptions, it introduces reconnection strategies to avoid a thundering herd problem, and it allows you to switch audio devices mid-call. Plus we really tried to make it unbreakable. There were days where we sat around trying to introduce (and mitigate) the weirdest connectivity problems.
The WebphoneLib in action
To put this new library to the test, we have been working with it in the new Webphone. It has been in use for a few months already. Of course it needs some more time in production before it is really battle-tested, but the stability introduced by this new lib seems promising.
Using this library, the frontend team can fully focus on the frontend. No need to think about complex event flows, the WebphoneLib does that for you.
See it for yourself – it’s open source!
This project is one of our first contributions to the Open Voip Alliance, where we hope to collaborate with other companies to develop useful software and to discuss and solve issues in the world of VoIP together.
My colleague Jos and I worked on this library together, and I’m really proud of the result. I loved taking a deep dive into SIP.js to figure out how it worked, to have discussions about how certain things should work, and to create as much stability as possible.
You can find the project on Github. Feel free to let us know what you think, and I hope this library makes your life easier too. Any contributions, feature requests or suggestions are more than welcome!
Your thoughts
No comments so far