Hire the author: Peter A

Introduction

AWS Kinesis video stream makes it easy to stream video securely from connected devices like mobile phones, laptops, desktops, tablet pc, etc. to AWS, for analysis, replay, and other user processing. You can stream video by using tools like AWS SDK Client and Amazon Kinesis Video Streams WebRTC.

Amazon Kinesis Video Streams is a fully managed AWS service that you can use to stream live video from devices to the AWS Cloud or build applications for real-time video processing or batch-oriented video analytics. It isn’t just storage for video data. You can use it to watch video streams in real-time from the cloud. You can also use it to monitor live streams in the AWS Management Console or for developing a monitoring application that uses the Kinesis Video Streams API library for displaying live video.

Glossary

Streaming media: Streaming media is multimedia that is delivered and consumed in a continuous manner from a source, with little or no intermediate storage in network elements. Streaming refers to the delivery method of content, rather than the content itself.

Cloud: The cloud refers to servers that are accessed over the Internet, and the software and databases that run on those servers.

SDK: A software development kit (SDK) is a collection of software development tools in one installable package.

Management Console: A terminal or workstation used to monitor and control a network either locally or remotely.

What we will be doing

In this tutorial, we will not be dealing with the setup of UI and related code for streaming live video. We will be explaining how to create a signed URL using the SigV4RequestSigner module of the AWS webrtc package. We will also be using the signed URL for streaming on clients without AWS credentials.

Prerequisites.

This tutorial will not be taking us through the basics of setting up video live stream, or kinesis video stream on the frontend. You need to have a basic understanding of the following

  1. Server-side Javascript programming knowledge with Nodejs and Express.js.
  2. Video streaming to AWS using AWS SDK Client and AWS Kinesis Video Streams WebRTC.
  3. RTCPeerConnection for video stream events.

Step 1 – Install our dependencies

We will first initialize a new npm project to set up our package.json file and then install our dependencies.

Run the code below in your terminal.

npm init

The code above will initialize a new npm project so that we can install our dependencies using the code below.

npm i -S express dotenv amazon-kinesis-video-streams-webrtc aws-sdk

Step 2 – Setup folder and create Express server

We will create an src folder in the root of the project and add an index.js file for express server.

In the /src/index.js add the code snippet below.

const express = require('express');
require('dotenv').config();
const { json, urlencoded } = express;
const app = express();
app.use(json());
app.use(urlencoded({ extended: true }));
app.get('/', (req, res) => {
return res.status(200).send({
message: 'Welcome to our AWS Kinesis Video Streaming API'
})
});
app.listen(3000, () => {
console.log(`app running on port: 3000`);
});
view raw index.js hosted with ❤ by GitHub

Step 3 – Setup AWS Kinesis Video Util file

We will create a file src/util/kinesis.js to set up our kinesis util file. We will be using a class base implementation because we will be creating multiple methods for this solution.

Copy the code in the snippet below to set up the kinesis util file.

const KinesisVideo = require('aws-sdk/clients/kinesisvideo');
const SigV4RequestSigner = require('amazon-kinesis-video-streams-webrtc').SigV4RequestSigner;
const KinesisVideoSignalingChannels = require('aws-sdk/clients/kinesisvideosignalingchannels');
require('dotenv').config();
const REQUEST_TIME_OUT_VALUE = 'TIMEOUT!';
const REQUEST_TIME_OUT_TIME = 15 * 1000;
const requestTimeout = (time, value) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(value);
}, time);
});
};
class KinesisUtil {
constructor () {
this.credential = {
region: process.env.KINESIS_REGION,
accessKeyId: process.env.KINESIS_ACCESS_KEY_ID,
secretAccessKey: process.env.KINESIS_SECRET_ACCESS_KEY
};
if (!this.kinesisClient) {
try {
this.kinesisClient = new KinesisVideo({this.credential });
} catch (err) {
console.error('Error creating Kinesis Client:', err.message);
};
}
}
async createChannel (ChannelName, role = 'VIEWER', clientId = null) {
try {
const result = { errorCode: 400 };
/* CHECK IF THE CHANNEL EXISTS */
const list = await this.kinesisClient.listSignalingChannels({
ChannelNameCondition: {
ComparisonOperator: 'BEGINS_WITH',
ComparisonValue: ChannelName
},
MaxResults: 1
}).promise();
if (list.ChannelInfoList.length) {
/* CHANNEL ALREADY EXISTS */
console.warn('Channel already exists:', ChannelName);
} else {
/* CREATE NEW CHANNEL */
await this.kinesisClient.createSignalingChannel({ ChannelName }).promise();
}
const describeSignalingChannelResponse = await this.kinesisClient.describeSignalingChannel({ ChannelName }).promise();
const channelInfo = describeSignalingChannelResponse.ChannelInfo;
const endpointsByProtocol = await this.listEndpoints(channelInfo.ChannelARN, role);
if (!endpointsByProtocol) {
result.errorCode = 404;
return result;
}
const iceServers = await this.listICEServers(channelInfo.ChannelARN, endpointsByProtocol.HTTPS);
if (!iceServers) {
result.errorCode = 404;
return result;
}
const configuration = {
iceServers,
iceTransportPolicy: 'all'
};
let queryParams = {
'X-Amz-ChannelARN': channelInfo.ChannelARN
};
if (clientId) {
queryParams = {
queryParams,
'X-Amz-ClientId': clientId
};
}
const signer = new SigV4RequestSigner(process.env.KINESIS_REGION, this.credential);
const url = await signer.getSignedURL(endpointsByProtocol.WSS, queryParams);
console.log('Kinesis created channel ARN:', channelInfo.ChannelARN);
const response = { configuration, url, role };
return response;
} catch (err) {
console.error('Error creating channel: ', err.message);
}
}
async listEndpoints (channelARN, role) {
const getSignalingChannelEndpoint = this.kinesisClient.getSignalingChannelEndpoint({
ChannelARN: channelARN,
SingleMasterChannelEndpointConfiguration: {
Protocols: ['WSS', 'HTTPS'],
Role: role
}
}).promise();
const getSignalingChannelEndpointResponse = await Promise.race([
getSignalingChannelEndpoint,
requestTimeout(REQUEST_TIME_OUT_TIME, REQUEST_TIME_OUT_VALUE)
]);
if (getSignalingChannelEndpointResponse === REQUEST_TIME_OUT_VALUE) {
console.error('getSignalingChannelEndpoint timeout!');
return null;
}
const endpointsByProtocol = getSignalingChannelEndpointResponse.ResourceEndpointList.reduce(
(endpoints, endpoint) => {
endpoints[endpoint.Protocol] = endpoint.ResourceEndpoint;
return endpoints;
}, {}
);
return endpointsByProtocol;
}
async listICEServers (channelARN, endpoint) {
const kinesisVideoSignalingChannelsClient = new KinesisVideoSignalingChannels({
this.credential, endpoint, correctClockSkew: true
});
const getIceServerConfig = kinesisVideoSignalingChannelsClient.getIceServerConfig({
ChannelARN: channelARN
}).promise();
const getIceServerConfigResponse = await Promise.race([
getIceServerConfig,
requestTimeout(REQUEST_TIME_OUT_TIME, REQUEST_TIME_OUT_VALUE)
]);
if (getIceServerConfigResponse === REQUEST_TIME_OUT_VALUE) {
console.error('getIceServerConfigResponse timeout!');
return null;
}
const iceServers = [];
getIceServerConfigResponse.IceServerList.forEach(iceServer =>
iceServers.push({
urls: iceServer.Uris,
username: iceServer.Username,
credential: iceServer.Password
})
);
return iceServers;
}
async deleteChannel (ChannelARN) {
try {
await this.kinesisClient.deleteSignalingChannel({ ChannelARN }).promise();
} catch (err) {
console.error('Error deleting channel: ', err.message);
}
}
}
module.exports = {
kinesis: new KinesisUtil()
};
view raw Kinesis.js hosted with ❤ by GitHub

In the code above, we created the class KinesisUtil with the following methods:

  1. createChannel: This method is for creating a new signaling channel if the channel name does not exist. It takes the parameters: channel name which is the name of the channel, role which is either MASTER or VIEWER, and the client id which can be null. The client Id can be null if the role is for the master but will be unique for each person connecting to the stream if the role is the viewer. This method also calls other methods like listEndpoints for listing the endpoints using the channelARN and role; and listICEServers for the server configuration
  2. listEndpoints: This method returns endpoints by protocol i.e HTTPS and WSS for web-socket.
  3. listIceServers: this method returns the ice servers which is then used in the configuration.

The createChannel method uses the SigV4RequestSigner method of amazon-kinesis-video-streams-webrtc package to return the signed URL to the client. With the signed URL, the client will not need to provide the AWS credentials to connect to the AWS kinesis video stream. The create channel method now returns the signed URL, configuration, and role to the client.

Step 4 – Update index.js and create .env file

Next, we now update our index.js file to include a new get route that returns the kinesis video data and also a .env file for our AWS credentials. Replace the code in src/index.js with the updated code below.

const express = require('express');
const { kinesis } = require('./util/kinesis');
require('dotenv').config();
const { json, urlencoded } = express;
const app = express();
app.use(json());
app.use(urlencoded({ extended: true }));
app.get('/', (req, res) => {
return res.status(200).send({
message: 'Welcome to our AWS Kinesis Video Streaming API'
})
});
app.get('/kinesis-video-url', (req, res) => {
const response = kinesis.createChannel('New_Tutorial_Channel', 'MASTER');
return res.status(200).json(response);
})
app.listen(3000, () => {
console.log(`app running on port: 3000`);
});

Next we create our .env file in the root of the project using the sample below.

KINESIS_REGION=
KINESIS_ACCESS_KEY_ID=
KINESIS_SECRET_ACCESS_KEY=
view raw env hosted with ❤ by GitHub

Step 5 – Running the app on backend

We can now start our server using

node src/index.js

From the client e.g react app, you can then make a get request to the backend on the endpoint

http://localhost:3000/kinesis-video-url

to fetch the signed URL on the client.

Step 6 – Setting up Kinesis Util on client/frontend

We can securely connect to AWS kinesis video stream without exposing AWS credentials on the client by simply using the signed URL from the backend endpoint. We can set up kinesis video util on the frontend following the sample below for both Master and Viewer connection.

import { SignalingClient } from 'amazon-kinesis-video-streams-webrtc';
const ERROR_CODE = {
OK: 0,
NO_WEBCAM: 1,
NOT_MASTER: 2,
VIEWER_NOT_FOUND: 3,
LOCAL_STREAM_NOT_FOUND: 4,
NOT_FOUND_ANY: 5,
ERROR: 6,
UNKNOWN_ERROR: 7
};
/**
* CustomSigner class takes the url in constructor with a getSignedUrl method which returns the signedURL
*/
class CustomSigner {
constructor (_url) {
this.url = _url;
}
getSignedURL () {
return this.url;
}
}
class KinesisUtil {
constructor () {
}
/**
*
* @param {*} kinesisInfo Kinesis data retrieved from the backend
*
* @param {*} clientId this should be a unique ID either user id or any unique id
* @returns
*/
async initializeViewer (kinesisInfo, clientId) {
const result = { errorCode: ERROR_CODE.UNKNOWN_ERROR }; /* IF THIS FUNCTION CANNOT REACH FINAL OK STATE -> ERROR */
const role = 'VIEWER';
this.clientId = clientId;
this.role = role;
const configuration = kinesisInfo.configuration;
if (this.signalingClient) {
this.signalingClient.close();
this.signalingClient = null;
}
this.signalingClient = new SignalingClient({
requestSigner: new CustomSigner(kinesisInfo.url), /** Use the customSigner to return the signed url, so we can ignore aws credentials
and regions, channelARN and channelEndpoint can be any text */
region: 'default region, (any text) as region is already part of signedurl',
role,
clientId,
channelARN: 'default channel, (any text) as channelARN is already part of signedurl',
channelEndpoint: 'default endpoint (any text) as endpoint is already part of signedurl'
});
this.peerConnection = new RTCPeerConnection(configuration);
this.signalingClient.on('open', async () => {
// code to run for open event
});
this.signalingClient.on('close', () => {
console.warn('Current signaling closed');
});
this.signalingClient.on('error', (e) => {
console.error('SignalingClient error:', e);
});
this.signalingClient.on('sdpOffer', async (offer, remoteClientId) => {
const peerConnection = new RTCPeerConnection(configuration);
this.peerConnectionByClientId[remoteClientId] = peerConnection;
// Other code to run
});
this.signalingClient.open();
result.errorCode = ERROR_CODE.OK;
return result;
}
async initializeMaster (kinesisInfo, localView, videoSize) {
const result = { errorCode: ERROR_CODE.UNKNOWN_ERROR }; /* IF THIS FUNCTION CANNOT REACH FINAL OK STATE -> ERROR */
const role = 'MASTER';
this.clientId = 'MASTER_ID';
this.role = role;
this.peerConnectionByClientId = {};
const configuration = kinesisInfo.configuration;
/** Custom Signer class with get signed url
* method to return the signed url from backend
* so other credentials can be left as default
* */
this.signalingClient = new SignalingClient({
requestSigner: new CustomSigner(kinesisInfo.url),
role,
region: 'default region, (any text) as region is already part of signedurl',
channelARN: 'default channel, (any text) as channelARN is already part of signedurl',
channelEndpoint: 'default endpoint (any text) as endpoint is already part of signedurl'
});
this.signalingClient.on('open', async () => {
// code to run on connection open
});
this.signalingClient.on('sdpOffer', async (offer, remoteClientId) => {
const peerConnection = new RTCPeerConnection(configuration);
this.peerConnectionByClientId[remoteClientId] = peerConnection;
// Other code to run
});
this.signalingClient.on('close', () => {
// code to run on connection close
});
this.signalingClient.on('error', (e) => {
// code to run on error
});
this.signalingClient.open();
/* LAST */
if (this.signalingClient) {
this.signalingClient.close();
this.signalingClient = null;
}
result.errorCode = ERROR_CODE.OK;
return result;
}
}
export {
KinesisUtil
};

The two methods in the client util file above take kinesisInfo params which is an object returned from our API call to the backend. It uses the info to connect to the video stream using the SignalingClient method of amazon-kinesis-video-streams-webrtc package. We also create a custom class CustomSigner which takes the signed URL in the constructor and returns the URL in the getSignedURL method of the class. The SignalingClient method takes a custom requestSigner property which returns the signed URL from our CustomSigner class. Using this approach, we can leave other credentials with any string, as kinesis will be connecting through the URL in our custom request signer which is similar to what the SignalingClient method does when we provide the AWS credentials.

Learning Tools

There are lots of things we can gain security wise by using the signed URL on clients for Kinesis video streams. To learn more about Kinesis Video stream and using Signed URL, we can check the links below

  1. Amazon Kinesis Video Developer Guide: https://docs.aws.amazon.com/kinesisvideostreams/latest/dg/what-is-kinesis-video.html
  2. SigV4RequestSigner class of Amazon Kinesis Video Streams Webrtc: https://github.com/awslabs/amazon-kinesis-video-streams-webrtc-sdk-js#class-sigv4requestsigner
  3. RTCPeerConnection: https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection.

Learning Strategy

I had to first learn how to connect to AWS kinesis video stream for Video live chat on both frontend and backend using AWS credentials. But then I understand that this method is not secure enough, because we have to expose the AWS credentials on the client which can leak one way or the other. So I took a step further to learn how to use Signed URL on the client as I couldn’t find any tutorial that explicitly discussed how to set up the client using Signed URL on the internet. There are tutorials that will discuss video streams on AWS using Socket live chat, but getting one that uses Signed URL is scarce even if there is any.

Reflective Analysis

The benefit of using Signed URL is huge, as in the past months, I have been working with applications that use Kinesis Video streams for live chat interviews. The project was setup using kinesis credentials on both frontend and backend, then I think of the security aspect, even though we secure the AWS credentials on the frontend using an env file, but a request will still be made on the frontend using these credentials which can expose it one way or the other. With this approach, you secure your AWS credentials better on the frontend without exposing it.

Conclusion

Kinesis Video stream is useful if you want to build applications that use live video. Examples like Video chats, Video interviews, etc. The complete code for this tutorial can be found here.

Hire the author: Peter A