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.
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.
1. Load index.html
2. Page asks for permission
3. Start media streaming
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/.
1. Page loaded in localhost:8080
2. Page requests for permission
3. Started media streaming
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.
But why?
This is because, browser features like user media can be used only under secure contexts.
A secure context is a
https://developer.mozilla.org/en-US/docs/Web/Security/Secure_ContextsWindow
orWorker
for which certain minimum standards of authentication and confidentiality are met.
In any insecure contexts, the API navigator.mediaDevices.getUserMedia
returns undefined
. As per the mozilla documentation, the below are considered to be secure contexts.
file:///
URL scheme- Page loaded with
localhost
- 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.
1. Page loaded in HTTPS
2. Page requests media permission
3. Media stream started 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