Hello everyone! During this COVID-19 pandemic working-from-home craze, everything has changed and moved to online. Almost all businesses postponed their back to office times until mid-2021, so this required all the meetings to be online. All events and schools have gone online, even friend meetings and house parties. With all these connectivity, you should know that you can build your very own basic online meeting tool very quickly and serverless! I’ll be demonstrating the steps and you can also check it out on Github and set it up for yourself later.
Basic Serverless Meeting app developed with Amazon Chine SDK.
github.com/sufleio/meeting-app-with-chime-sdkLet’s open our terminal and get started!
mkdir meeting-app
cd meeting app/
npm init -y
We need:
Why server-side code? Because we need to use aws-sdk
to provision our Chime resources (meetings, attendee management etc.). Aws-chime-sdk-js provides us client management, such as connecting client inputs, showing videos etc, more of meeting’s client state management.
The scenario will be:
Let’s get started! We start by adding aws-sdk
to our project, with the uuid
package for id generation as well. We are also creating our handler.
yarn add aws-sdk uuid
touch handlers.js
Start coding by creating our join
and end
endpoints.
const AWS = require('aws-sdk');
const chime = new AWS.Chime();
// Set the AWS SDK Chime endpoint. The global endpoint is https://service.chime.aws.amazon.com.
chime.endpoint = new AWS.Endpoint('https://service.chime.aws.amazon.com');
const json = (statusCode, contentType, body) => {
return {
statusCode,
headers: { "content-type": contentType },
body: JSON.stringify(body),
};
};
exports.join = async (event, context, callback) => {
}
exports.end = async (event, context, callback) => {
}
Since I’m planning to use node12.x runtime, I can use async/await and other great features ES6 brings. I’ve also added a helper json() response method. Now, for the join endpoint, we will take a client id that is unique to that client and a meeting id if that client is attending an existing meeting.
exports.join = async (event, context, callback) => {
const query = event.queryStringParameters;
let meetingId = null;
let meeting = null;
if (!query.meetingId) {
//new meeting
meetingId = uuidv4();
meeting = await chime.createMeeting({
ClientRequestToken: meetingId,
MediaRegion: "eu-west-1", //I’ll use eu-west-1 in this demo. You can make your user pick the region in the client, or even get nearest media region from endpoint https://nearest-media-region.l.chime.aws
ExternalMeetingId: meetingId,
}).promise();
} else {
//join to existing meeting
meetingId = query.meetingId;
meeting = await chime.getMeeting({
MeetingId: meetingId,
}).promise();
}
//We've initialized our meeting! Now let's add attendees.
const attendee = await chime
.createAttendee({
//ID of the meeting
MeetingId: meeting.Meeting.MeetingId,
//User ID that we want to associate to
ExternalUserId: `${uuidv4().substring(0, 8)}#${query.clientId}`,
})
.promise();
return json(200, "application/json", {
Info: {
Meeting: meeting,
Attendee: attendee,
},
});
};
We’ve initialized meetings as we defined in our code and our attendees can now join. Now, let’s write the end
endpoint to delete the meeting.
exports.end = async (event, context) => {
const body = JSON.parse(event.body);
await chime.deleteMeeting({
MeetingId: body.meetingId
}).promise();
return json(200, "application/json", {});
};
We will take a meetingId
from the body, pass it to the deleteMeeting
endpoint of Chime SDK.
Next step, we need to test these functions locally. We will test and deploy our functions via SAM CLI. If you haven’t installed it yet, you can see installation guide from here.
Let’s create our template:
# template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Description: Serverless Meetings with Amazon Chime Demo
Globals:
Function:
Runtime: nodejs12.x
Timeout: 30
MemorySize: 128
Resources:
# Create Chime Resources Access Policy
ChimeMeetingsAccessPolicy:
Type: AWS::IAM::Policy
Properties:
PolicyName: ChimeMeetingsAccess
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- 'chime:CreateMeeting'
- 'chime:DeleteMeeting'
- 'chime:GetMeeting'
- 'chime:ListMeetings'
- 'chime:BatchCreateAttendee'
- 'chime:CreateAttendee'
- 'chime:DeleteAttendee'
- 'chime:GetAttendee'
- 'chime:ListAttendees'
Resource: '*'
Roles:
# Which lambda roles will have this policy
- Ref: MeetingJoinLambdaRole
- Ref: MeetingEndLambdaRole
MeetingIndexLambda:
Type: 'AWS::Serverless::Function'
Properties:
Handler: handlers.index
Events:
Api1:
Type: Api
Properties:
# since it is a static page, proxy+ will redirect all resources to that endpoint
Path: /{proxy+}
Method: GET
Auth:
# If you don't specify an empty authorizer, API Gateway requests an API Key by default
Authorizer: null
# Join function definition
MeetingJoinLambda:
Type: AWS::Serverless::Function
Properties:
Handler: handlers.join
Events:
Api1:
Type: Api
Properties:
Path: /join
Method: POST
# End function definition
MeetingEndLambda:
Type: AWS::Serverless::Function
Properties:
Handler: handlers.end
Events:
Api1:
Type: Api
Properties:
Path: /end
Method: POST
Outputs:
ApiURL:
Description: "API endpoint URL for Prod environment"
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/"
I’ve already added some comments to code, but to be more specific I’ve added join and end function definitions and a proxy+ definition for /. This will enable us to serve our html/css/js/image content through serverless-aws-static-file-handler.
For testing, we will build our application first and then start an api-gateway like api for local testing. Go to terminal and type:
sam build
sam local start-api
build
will install and prepare all of our three functions. local start-api
will host them locally. You can access your functions now. Let’s open Postman and make a join meeting request:
You can use console.log which will dump your log to runtime console:
It’s time to build our client! It will be really simple (no React/Vue etc) a single html page with only a few styles and scripts. You can always start from this blog case and make something much greater. To be able to serve the static files, I’ll add this awesome package called serverless-aws-static-file-handler
to our project and change the index handler’s path to (proxy+), so that it can serve all static files. Finally, we will move our static content under a folder. I’ve called it html, and serve all content from there.
Let’s add our index.html and our index function handler:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Serverless Meetings</title>
</head>
<body>
<h1>Welcome to Serverless Meetings!</h1>
<audio style="display: none" id="meeting-audio"></audio>
<button type="button" id="start-button" style="display: none;"></button>
<div id="video-list">
</div>
<script src="assets/js/app.js"></script>
</body>
</html>
const StaticFileHandler = require('serverless-aws-static-file-handler')
exports.index = async (event, context, callback) => {
const clientFilesPath = __dirname + "/html/";
const fileHandler = new StaticFileHandler(clientFilesPath)
return await fileHandler.get(event,context);
}
This handler will serve all static files for us. You can check out the original handler from link. It is originally built for use with “serverless framework”, but you can install the package and use it in your SAM.
Next up, I’ll pack chime js sdk into a single js file. You can check on how to do it here. Let’s add it under /assets/js and also add an app.js file for application files. In standart WebRTC scenarios you have to handle all RTCPeerConnection handling, SDP’s, local and remote descriptions and a bunch of other things. With Chime, there are just a few things you need to do:
While this looks too simple to be true, here is the code from my app.js as a proof:
async function start() {
try {
let requestPath = `join?clientId=${clientId}`;
if (!meetingId) {
isMeetingHost = true;
} else {
requestPath += `&meetingId=${meetingId}`;
}
var response = await fetch(requestPath, {
method: "POST",
headers: new Headers(),
});
const data = await response.json();
meetingId = data.Info.Meeting.Meeting.MeetingId;
if (isMeetingHost) {
document.getElementById("meeting-link").innerText = window.location.href + "?meetingId=" + meetingId;
}
const configuration = new ChimeSDK.MeetingSessionConfiguration(
data.Info.Meeting.Meeting,
data.Info.Attendee.Attendee
);
window.meetingSession = new ChimeSDK.DefaultMeetingSession(
configuration,
logger,
deviceController
);
const audioInputs = await meetingSession.audioVideo.listAudioInputDevices();
const videoInputs = await meetingSession.audioVideo.listVideoInputDevices();
await meetingSession.audioVideo.chooseAudioInputDevice(
audioInputs[0].deviceId
);
await meetingSession.audioVideo.chooseVideoInputDevice(
videoInputs[0].deviceId
);
const audioOutputElement = document.getElementById("meeting-audio");
meetingSession.audioVideo.bindAudioElement(audioOutputElement);
meetingSession.audioVideo.start();
} catch (err) {
console.error(err);
}
}
Since we are using Chime js sdk’s browser version, there’s no bundling or module management. All Chime SDK functionality is exposed via window.ChimeSDK object.
Here are a few things about the code:
<audio />
element in your page, naturally. You will not be able to hear the incoming audio stream without an audio element,Let’s deploy our application and check if it works!
Go to terminal and write:
sam build
sam deploy --guided
build will install dependencies and package our application. deploy will upload packaged code, and check our resources, --guided option will ask you for stack name, bucket name etc. It’s good to do it on the first run. It will also save options that you’ve selected in a samconfig.toml file, so it will remember your chosen options afterwards. Finally when everything is ready, changes will be listed to you.
If everything looks ok, go on and confirm the changeset. Grab the resulting url, add an /index.html at the end and open it in a new window, click Start. Grab your generated meeting id, append and open it in a new window, click Join.
Voila! You will be able to hear yourself! It really is that simple! You don’t even have to manage disabling your voice etc, Chime already doesn’t let you hear yourself.
Let’s do our video previews now. When you’ve clicked start, you may have realized that your cam light started lighting. It was streaming already, but we didn’t configure any video previews and you didn’t see anyone. Let’s start building that.
In Chime SDK, there are no video endpoints etc. By design, each video stream preview is called VideoTile. When a video preview is available, you can access and list all of them via meetingSession.audioVideo.getAllVideoTiles()
function. How do you make it available on screen? Well, you bind the tile to a video element.
const tiles = meetingSession.audioVideo.getAllVideoTiles();
tiles.forEach(tile => {
let tileId = tile.tileState.tileId
var videoElement = document.getElementById("video-" + tileId);
if (!videoElement) {
videoElement = document.createElement("video");
videoElement.id = "video-" + tileId;
document.getElementById("video-list").append(videoElement);
meetingSession.audioVideo.bindVideoElement(
tileId,
videoElement
);
}
})
I’ve planned to add video elements on the fly. So, I’ve decided to create them for each videotile, then add it to DOM, bind them to AudioVideo facade after that.
But it doesn’t add it to the page automatically. We need to observe when a videoTile is updated. How do we do that? The AudioVideo facade has an observer mechanism which has all the events you can use in your meeting and you can subscribe to it. You can check all of the events from here. We will create an observer object and make use of videoTileDidUpdate event. Our start function looks like this after the update:
async function start() {
if (meetingSession) {
return
}
try {
var response = await fetch(requestPath, {
method: "POST",
headers: new Headers(),
});
const data = await response.json();
meetingId = data.Info.Meeting.Meeting.MeetingId;
if (isMeetingHost) {
document.getElementById("meeting-link").innerText = window.location.href + "?meetingId=" + meetingId;
}
const configuration = new ChimeSDK.MeetingSessionConfiguration(
data.Info.Meeting.Meeting,
data.Info.Attendee.Attendee
);
window.meetingSession = new ChimeSDK.DefaultMeetingSession(
configuration,
logger,
deviceController
);
const audioInputs = await meetingSession.audioVideo.listAudioInputDevices();
const videoInputs = await meetingSession.audioVideo.listVideoInputDevices();
await meetingSession.audioVideo.chooseAudioInputDevice(
audioInputs[0].deviceId
);
await meetingSession.audioVideo.chooseVideoInputDevice(
videoInputs[0].deviceId
);
const observer = {
videoTileDidUpdate: (tileState) => {
// Ignore a tile without attendee ID and other attendee's tile.
if (!tileState.boundAttendeeId) {
return;
}
updateTiles(meetingSession);
},
};
meetingSession.audioVideo.addObserver(observer);
const audioOutputElement = document.getElementById("meeting-audio");
meetingSession.audioVideo.bindAudioElement(audioOutputElement);
meetingSession.audioVideo.start();
} catch (err) {
console.error(err);
}
}
function updateTiles(meetingSession) {
const tiles = meetingSession.audioVideo.getAllVideoTiles();
tiles.forEach(tile => {
let tileId = tile.tileState.tileId
var videoElement = document.getElementById("video-" + tileId);
if (!videoElement) {
videoElement = document.createElement("video");
videoElement.id = "video-" + tileId;
document.getElementById("video-list").append(videoElement);
meetingSession.audioVideo.bindVideoElement(
tileId,
videoElement
);
}
})
}
Let’s deploy our functions and update the application!
sam build
sam deploy
Open the link, start a meeting and send the link to your friends, celebrate being together online!
A screenshot from our weekly team meeting in the serverless meeting platform we just built!
Chime SDK’s total attendee limit is 250. Which means you can start 250 audio streams, but only 16 video streams per meeting. You can check the details from this document.
This powerful SDK with easy integration is fairly cheap (to my sense). Chime SDK’s pricing is separate from Amazon Chime’s pricing, and it has a pricing per minute per attendee equal to $0.0017. So when you talk to your 10 friends for 1 hour, you will be charged 10 x 60 x$ 0.0017 = $1.02
It is awesome to see that Chime SDK leverages a lot of things and makes developing meeting applications a breeze. I didn’t go into more details here, but you can make use of Chime’s many features like screen sharing, content sharing or even broadcast live events with Chime and Elemental integration (example).
I’m planning to deep dive into Chime and create a few more detailed and full scale applications really soon. Stay tuned!
An experienced software engineer, Durul is indeed a technology lover and always excited to see and learn what technology offers. With his experience in startups from Entertainment to SaaS, he is keen on sharing his experience with the technology community.
Subscribe to Our Newsletter
Our Service
Specialties
Copyright © 2018-2024 Sufle
We use cookies to offer you a better experience with personalized content.
Cookies are small files that are sent to and stored in your computer by the websites you visit. Next time you visit the site, your browser will read the cookie and relay the information back to the website or element that originally set the cookie.
Cookies allow us to recognize you automatically whenever you visit our site so that we can personalize your experience and provide you with better service.