WebRTC, Typescript, & Bandwidth
Bandwidth has the most robust communication APIs available for large scale voice and messaging applications. This is a tutorial on building a small WebRTC application using Node and Typescript in the backend and simple Javascript in the front.
Instead of typing out our reasonings for using typescript, I'll refer you to Victor Savkin's excellent Blog Post detailing many reasons why Angular 2 uses Typescript.
This app touches on:
- Ordering Phone Numbers
- Creating Bandwidth Application and setting callbacks
- Creating Bandwidth SIP Domain
- Creating Bandwidth SIP Endpoint
- Audio Playback
- Call Recording
- Call Recording Fetch
- Incoming Call Handling
- Outbound SIP Call Handling
The Bandwidth Application is a resource used to keep track of callback urls for batches of phone numbers. As part of the 'one click deploy' we create the Bandwidth application on first visit and appropriately set the callback url.
private async getApplicationId(ctx: IContext): Promise<string> {
const host = ctx.request.host;
debug(`Creating new application with callback ${buildAbsoluteUrl(ctx, '/callCallback')}`);
application = await this.catapult.Application.create({
name: appName,
autoAnswer: true,
incomingCallUrl: buildAbsoluteUrl(ctx, '/callCallback')
});
applicationIds.set(host, application.id);
debug(`Using new application id ${application.id}`);
return application.id;
}As part of new user creation, the user requests a phone number in a certain area code.
async createPhoneNumber(ctx: IContext, areaCode: string): Promise<string> {
debug(`Reserving a new phone number for area code ${areaCode}`);
const applicationId = await this.getApplicationId(ctx);
debug(`Search and order available number`);
const numbers = await this.catapult.AvailableNumber.searchAndOrder('local', { areaCode, quantity: 1 });
await this.catapult.PhoneNumber.update(numbers[0].id, { applicationId });
return numbers[0].number;
}Bandwidth has it's own SIP registrar. In order to connect use web voice, each account must create a domain. Once the domain has been created, as many endpoints can be created on the domain. Each time a user registers, a new endpoint is created for the user.
private async getDomain(ctx: IContext): Promise<IDomainInfo> {
const name = randomstring.generate({
length: 1,
charset: 'alphabetic'
}) + randomstring.generate({
length: 14,
charset: 'alphanumeric'
});
debug(`Creating new domain ${name}`);
domain = await this.catapult.Domain.create({ name, description });
debug(`Using new domain info for ${domain.name}`);
return getDomainInfo(domain);
}
}Each endpoint needs to use the applicationId set above to ensure any events are sent to your server.
async createSIPAccount(ctx: IContext): Promise<ISIPAccount> {
const applicationId = await this.getApplicationId(ctx);
const domain = await this.getDomain(ctx);
const sipUserName = `vu-${randomstring.generate(12)}`;
const sipPassword = randomstring.generate(16);
debug('Creating SIP account');
const endpoint = await this.catapult.Endpoint.create(domain.id, {
applicationId, //ID above
domainId: domain.id,
name: sipUserName,
description: `${applicationName}'s SIP Account`,
credentials: { password: sipPassword }
});
return <ISIPAccount>{
endpointId: endpoint.id,
uri: `sip:${sipUserName}@${domain.name}.bwapp.bwsip.io`,
password: sipPassword
};
}Each Callback event is routed to the backend server and must be handled.
{
"eventType":"answer",
"from":"sip:<endpoint>@<domaind>.bwapp.bwsip.io",
"to":"+13865245000",
"callId":"{call-id}",
"callUri": "https://api.catapult.inetwork.com/v1/users/{user-id}/calls/{call-id}",
"callState":"active",
"time":"2012-11-14T16:28:31.536Z"
}Both calls FROM the SIP Endpoint AND calls from the telephone network appear to the server as incoming calls. It's important to check the from value in the incoming call payload to see if it's from the SIP Endpoint.
If the call is FROM the sip endpoint, we can simply use the transfer function to send the call the telephone network
if (from === user.sipUri) {
debug(`Transfering outgoing call to ${to}`);
await api.transferCall(callId, to, user.phoneNumber);
return;
}In order to preserve the caller ID, the first call must first be put into a bridge . Then the call to the SIP Endpoint can be created with the bridgeID and the original caller ID.
if (to === user.phoneNumber) {
debug(`Bridging incoming call with ${user.sipUri}`);
const callerId = await getCallerId(models, from);
await api.playAudioToCall(callId, tonesURL, true, '');
debug(`Using caller id ${callerId}`);
const bridgeId = await api.createBridge({
callIds: [callId],
bridgeAudio: true
});
// save current call data to db
await models.activeCall.create({
callId,
bridgeId,
user: user.id,
from,
to,
});
debug(`Calling to another leg ${user.sipUri}`);
const anotherCallId = await api.createCall({
bridgeId,
from: callerId,
to: user.sipUri,
tag: `AnotherLeg:${callId}`,
callTimeout: 10,
callbackUrl: buildAbsoluteUrl(ctx, `/callCallback`),
});
// save bridged call data to db too
await models.activeCall.create({
callId: anotherCallId,
bridgeId,
user: user.id,
from: callerId,
To: user.sipUri
});
}Works on:
- Firefox
- Chrome
- Opera
For full support check iswebrtcreadyyet.com
Full docs are here CDN hosted library:
var callOptions = {
mediaConstraints: {
audio: true, // only audio calls
video: false
}
};
var bwPhone = new JsSIP.UA({
'uri': 'sip:[email protected]',
'password': 'password',
'ws_servers': 'wss://webrtc.registration.bandwidth.com:10443'
});
bwPhone.start();
bwPhone.on("registered", function(){
bwPhone.call("222-333-4444", callOptions);
});
bwPhone.on("newRTCSession", function(data){
var session = data.session; // outgoing call session here
var dtmfSender;
session.on("confirmed",function(){
//the call has connected, and audio is playing
var localStream = session.connection.getLocalStreams()[0];
dtmfSender = session.connection.createDTMFSender(localStream.getAudioTracks()[0])
});
session.on("ended",function(){
//the call has ended
});
session.on("failed",function(){
// unable to establish the call
});
session.on('addstream', function(e){
// set remote audio stream (to listen to remote audio)
// remoteAudio is <audio> element on page
remoteAudio.src = window.URL.createObjectURL(e.stream);
remoteAudio.play();
});
//play a DTMF tone (session has method `sendDTMF` too but it doesn't work with Catapult server right)
dtmfSender.insertDTMF("1");
dtmfSender.insertDTMF("#");
//mute call
session.mute({audio: true});
//unmute call
session.unmute({audio: true});
//to hangup the call
session.terminate();
});
var callOptions = {
mediaConstraints: {
audio: true, // only audio calls
video: false
}
};
var bwPhone = new JsSIP.UA({
'uri': 'sip:[email protected]',
'password': 'password',
'ws_servers': 'wss://webrtc.registration.bandwidth.com:10443'
});
bwPhone.start();
bwPhone.on("newRTCSession", function(data){
var session = data.session;
if (session.direction === "incoming") {
// incoming call here
session.on("accepted",function(){
// the call has answered
});
session.on("confirmed",function(){
// this handler will be called for incoming calls too
});
session.on("ended",function(){
// the call has ended
});
session.on("failed",function(){
// unable to establish the call
});
session.on('addstream', function(e){
// set remote audio stream (to listen to remote audio)
// remoteAudio is <audio> element on page
remoteAudio.src = window.URL.createObjectURL(e.stream);
remoteAudio.play();
});
// Answer call
session.answer(callOptions);
// Reject call (or hang up it)
session.terminate();
}
});To get started, head over to Bandwidth and create your account. From there head over to the Github Page for in depth code and deployment instructions

