getUserMedia API for WebRTC

getUserMedia API – An introduction

During this pandemic, social-distancing became the new normal which increased digital communication. With this in mind, I wanted to explore on how video conference apps work. So I started a side project with WebRTC and decided to write a series of blog posts on it. In this post, I’ll cover the basic part in WebRTC to get user’s video and audio from the webcam and mic using the getUserMedia API.

Understanding the browser API getUserMedia

We can access the multimedia stream easily by using the browser API navigator.mediaDevices.getUserMedia. Let’s see what this code does.

navigator

The Navigator interface represents the state and the identity of the user agent. It allows scripts to query it and to register themselves to carry on some activities.

https://developer.mozilla.org/en-US/docs/Web/API/Navigator

The navigator interface in the browser provides functions and properties to access browser’s state and features by which, we can get the browser’s state or access a wide range of functionality. For example, by using navigator.onLine we can get browser’s connection state with the internet.

navigator.onLine in action
navigator.onLine in action

mediaDevices

The Navigator.mediaDevices read-only property returns a MediaDevices object, which provides access to connected media input devices like cameras and microphones, as well as screen sharing.

https://developer.mozilla.org/en-US/docs/Web/API/Navigator/mediaDevices

The above definition from mozilla document is self explanatory. The functions in mediaDevices object provide features like screen sharing, getting streams from camera and microphones. The function getUserMedia is the one which we need to get the streams from camera and microphones.

Using getUserMedia

Let’s create a sample project to understand how to use getUserMedia.

The sample project

I created a HTML and a JavaScript file to capture and play the stream from the camera and the microphone.

Folder structure

usermedia
  -- index.html
  -- /js
    -- script.js 

Code

<!-- index.html -->
<!doctype html>
<html>
  <head>
    <title>Using getUserMedia</title>
  </head>
  <body>
    <video id="localVideo" autoplay playsinline></video>
    <br/>
    <button id="openCamera">Start</button>
    <button id="stopCamera">Stop</button>

    <div id="error" style="color: red;"></div>

    <script type="text/javascript" src="./js/script.js"></script>
  </body>
</html>

In the above HTML file, I added a video element with attribute id="localVideo" in which I’ll be loading the video stream from the function getUserMedia. I have added two buttons, ‘Start’ and ‘Stop’ which I’ll use to start and stop the media streaming.

// script.js
let streamGlobal;

document.getElementById('openCamera').addEventListener('click', e => openCamera(e));
document.getElementById('stopCamera').addEventListener('click', e => stopCamera(e));

function stopCamera() {
	if (!streamGlobal) {
		console.warn('no stream to stop!');
		return;
	}
	streamGlobal.getTracks().forEach(function(track) {
		track.stop();
	});  
}

function openCamera() {
	const localVideoElem = document.getElementById('localVideo');
	navigator.mediaDevices.getUserMedia({video: true, audio: true}).then(stream => {
		localVideoElem.srcObject = stream;
		streamGlobal = stream;
	}).catch(e => {
		console.error(e);
		const errorElem = document.getElementById('error');
		errorElem.innerText = e;
	});
}

In the above JS file, I have obtained the media stream using navigator.mediaDevices.getUserMedia API. I have passed the constraint {video: true, audio: true} to get both video and audio from camera and microphone. Check here for more constraints. This API returns a media stream which I’ll load into the video element by id localVideo.

Let’s run the index.html in a browser.

Loaded index.html

1. Load index.html

Page asks for permission

2. Page asks for permission

start media stream using getUserMedia

3. Start media streaming

stopped media stream obtained from getUserMedia

4. Stop media streaming

Great, it works. When I start the media streaming by clicking ‘Start’ button, the API requests for user permission. After I click ‘Allow’, the media is streamed in the video element. On clicking the ‘Stop’ button, the stream is stopped.

But what good is a local html file? We need to setup a server to access this from other devices.

Creating a server

To setup a server, I converted this to a node project and installed Express JS to create a server. To convert it into a node project, simply run the command npm init -y. This will setup a quick node project and you will now have a package.json file. Install express with the command npm i express. This will install Express JS and add dependency entry in package.json. I have moved the source files into public folder. I created index.js file to add server related codes.

Folder structure

usermedia
  -- index.js
  -- package.json
  -- /public
    -- index.html
    -- /js
      -- script.js

Now lets code the server. I have added the below code in index.js.

// index.js
const express = require('express');
const path = require('path');

const app = express();
const httpPort = 8080;

// To serve index.html and script.js as static files.
app.get('/', express.static(path.join(__dirname, '/public')));
app.use("/js", express.static(path.join(__dirname, '/public/js')));

app.listen(httpPort, () => {
  console.log(`Express HTTP Server running on port ${httpPort}`);
});

Let’s try this out. Run the command node index.js to start the server and open http://localhost:8080/.

localhost loaded

1. Page loaded in localhost:8080

API asks for user permission

2. Page requests for permission

Localhost video streaming started using getUserMedia

3. Started media streaming

Localhost video streaming stopped

4. Stopped media streaming

Great this works too!

But..

When we access from a different system through the local network, http://localhost:8080/ doesn’t work does it? So let’s use the local IP address to test this out. My local IP is 192.168.0.147 and so I’m going to try the URL http://192.168.0.147:8080/. You guys should try with your local IP. When I load the page and click ‘Start’ button, it doesn’t work and we can find the error in the browser console as shown in below image.

getUserMedia throw error when page loaded in insecured context

But why?

This is because, browser features like user media can be used only under secure contexts.

secure context is a Window or Worker for which certain minimum standards of authentication and confidentiality are met.

https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts

In any insecure contexts, the API navigator.mediaDevices.getUserMedia returns undefined. As per the mozilla documentation, the below are considered to be secure contexts.

  1. file:/// URL scheme
  2. Page loaded with localhost
  3. A site secured with HTTPS

This is why our example worked when loaded as a file under file:/// URL scheme or as a page under localhost. It didn’t work when we tried to access with local IP address without HTTPS. Time to configure HTTPS now.

Adding self signed SSL certificate

For HTTPS in our local environment, lets create and add a self signed SSL certificate. There are tons of pages on how to create a self signed SSL certificate for your local environment. I prefer this one by Digital Ocean. Go through this for a better understanding. For quick creation, execute the below commands.

openssl req -x509 -nodes -new -sha256 -days 1024 -newkey rsa:2048 -keyout <key name>.key -out <pem name>.pem -subj "/C=US/CN=localhost-CA"
openssl x509 -outform pem -in <pem name>.pem -out <cert name>.crt

This will generate three files. Now let’s add these to our server. I have modified the index.js to below.

// index.js
const express = require('express');
const https = require('https');
const http = require('http');
const fs = require('fs');
const path = require('path');

const app = express();
const httpPort = 8080;
const httpsPort = 1443; // ports below 1024 require root permission.

const httpServer = http.createServer(app);
const httpsServer = https.createServer({
  key: fs.readFileSync('<path to .key file>/xyz.key '),
  cert: fs.readFileSync('<path to .crt file>/xyz.crt'),
}, app);

// To serve index.html and script.js as static files.
app.get('/', express.static(path.join(__dirname, '/public')));
app.use("/js", express.static(path.join(__dirname, '/public/js')));

httpServer.listen(httpPort, () => {
    console.log(`HTTP Server running on port ${httpPort}`);
});

httpsServer.listen(httpsPort, () => {
    console.log(`HTTPS Server running on port ${httpsPort}`);
});

Note that I have used the port number 1443 for https instead of the standard port 443 as ports below 1024 requires root permission in linux. Start the server by running the command node index.js and open https://<your_local_ip>:1443/ in your web browser. If your browser warns you to go back because the site is not secured, it’s ok to proceed. We used a self signed certificate which is not signed by a Certificate Authority to test in the local environment. Let’s see how this works.

Page loaded in local IP

1. Page loaded in HTTPS

Page requests permission

2. Page requests media permission

Video stream started in https using getUserMedia

3. Media stream started in HTTPS

Video stream stopped in https

4. Media stream stopped in HTTPS

Finally, that works! Now we have a sharable URL which can be accessed through your local network.

The source code of the example project used can be found in this Github repo.

What’s next?

Check how to secure your site from accessing browser features here.

Checkout our other tutorials here.


I hope this post was helpful 😊. If you find this post informative, support us by sharing this with fellow programmers in your circle 😀.

For any suggestions, improvements, reviews or if you like us to cover a specific topic, please leave a comment.
Follow us on twitter @thegeeksclan and in Facebook.
#TheGeeksClan #DevCommunity

error

Enjoy this blog? Please spread the word :)