Skip to content

Instantly share code, notes, and snippets.

@dtolb
Last active August 1, 2016 20:28
Show Gist options
  • Select an option

  • Save dtolb/935511b79ee1952fbe9308d2f47fdc06 to your computer and use it in GitHub Desktop.

Select an option

Save dtolb/935511b79ee1952fbe9308d2f47fdc06 to your computer and use it in GitHub Desktop.
nodeweekly

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.

Technologies used

Deploy

ScreenShot

Why Typescript

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.

Application Overview

This app touches on:

Creating Bandwidth Application

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;
	}

Searching and ordering phone number

As part of new user creation, the user requests a phone number in a certain area code.

Step 1

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;
}

Creating the SIP Domain and Endpoints

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.

Create new domain

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);
	}
}

Create endpoint for users

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
  };
}

Handling Callbacks Events

Each Callback event is routed to the backend server and must be handled.

Handling Incoming Call

{
   "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.

From 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;
}
From telephone network

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
	});
}

WebRTC and Bandwidth

Works on:

  • Firefox
  • Chrome
  • Opera

For full support check iswebrtcreadyyet.com

Full docs are here ​ CDN hosted library:

Creating Outbound Call

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();});

Inbound Call

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();
    }
});

Further Reading

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment