Streaming user video on the web


This is a demonstration of how to stream user video on the web. If you want to cut to the chase and see your beautiful face on screen, skip to here.


While there are plenty of apps that will take video for you—including video-chat apps like Zoom, video-sharing apps like Instagram and TikTok, or just the camera app on your smartphone—it's fairly straightforward to use native web API's to create a video-streaming web app yourself. This opens up all sorts of possibilities, like creating your own web-based video-sharing app, or just doing fun stuff with video on the web.

Here I'll use the Media Devices web API to stream a user's web camera and play it back. The techniques I use come largely from these docs. If you want, you can build a web-camera streaming app yourself as you follow along. You'll just need a basic web project such as a Vite app, or you can use this starter template.

Now let's jump into the stream.

Streaming video element

The first thing you need to stream video is a way to show it to the user. For that you'll need an HTML video element.

I'll give it an id of "videoStream" to make it easier to grab the element and attach the stream to it later. Tbe video element can look like this your HTML:

<video id="videoStream"></video>

Depending on the context you're streaming in, you can add attributes to that element such as muted to mute the stream by default, or controls, if you want the user to be able to use the default video controls (like pausing/playing and adjusting volume). You could also attach CSS styles to the video element to size it.

Getting user media

Now let's get the user's stream and play it on that video element. This will be done in JavaScript, e.g. in a script.js file.

To get the user's stream, we can simply use the getUserMedia method. That method can be called like this: navigator.mediaDevices.getUserMedia(constraints).

Since we need to wait for the user to approve our app to stream their video (if they haven't already) and then activate the stream, this is an asynchronous operation, meaning it won't return instantly. It therefore returns a Promse, which, if you're not familiar, is a JavaScript concept for something that's in waiting. When a promise is fulfilled (or rejected if there is a failure), we want to run a function in response, which is called a callback since it gets called once another function has finished.

You can put the "success callback" or the function that runs once the promise is resolved successfully in the then() method of the promise. The then() method can also take a second argument for when the promise is rejected.

You can also handle errors in a final catch() method chained to the promise, which is where I'll put a "failure callback". The promise might fail if the user declines to let the app stream their video, or if there was an issue getting the stream from their device.

So the code to get the user's stream will end up looking like this, with the successCallback being the function that will run if it's a success and the failureCallback if it fails. We'll define those functions in a little bit.

navigator.mediaDevices.getUserMedia(constraints).then(successCallback).catch(failureCallback);

Stream constraints

Before asking the user permission to use their camera, you can set some constraints on the kind of media/device you'd prefer them to stream. These will go in the constraints argument of the getUserMedia method.

The constraints object can specify both video and audio. If you don't have any contraints in mind, then you can just say true for each and whatever audio or video the user has set up by default will be used.

const constraints = {
  video: true,
  audio: true,
}

If you want, can specifiy ideal dimensions for the video such as 1,280 x 720. You can also specify the "facing mode" or whether the camera should face the user or their environment. "Environment" here means facing away from the user, not that they are actually out in the wild while streaming (but they could be).

const constraints = {
  audio: true,
  video: {
    width: 1280,
    height: 720,
    facingMode: "user",
  }
}

By default, the contraints are "ideal", meaning they don't need to exactly match the user's device to stream. If you specify "exact", then the user must have a device that matches those criterion or the stream won't start.

const constraints = {
  audio: true,
  video: {
    facingMode: { exact: "environment" },
    width: { exact: 1280 },
    height: { exact: 720 },
  }
}

I'll just stick with "ideal" constraints so a broader range of user devices can stream.

Success and failure

Now that we have the constraints, we need to define our success and failure callback functions.

If the getUserMedia() promise resolves successfully, it means the user approved it and the stream has started. This will return a stream which we can then attach to our video element so the user can see it. You can attach it to the video element by setting it as that element's srcObject. We can grab the video element by using the getElementById() method.

So the success callback function would look like this:

function successCallback(stream) {
    const streamElement = document.getElementById('videoStream');
    streamElement.srcObject = stream;
}

This attaches the stream to our video element and loads the stream. To actually play the stream once it's loaded, we need to call the play() method on the video element.

A loadedmetadata event will fire on the video element once it's ready to play, so we can listen for that event and then play the stream once it fires.

Now the full success callback looks like this:


function successCallback(stream) {
    const streamElement = document.getElementById('videoStream');
    streamElement.srcObject = stream;
    streamElement.addEventListener('loadedmetadata', (metadata) => {
        // play stream once 'loadedmetadata' event fires
        streamElement.play();
    });
}

Once the stream metadata loads, we can also get the stream's width and height which can be accessed by metadata.srcElement.videoWidth and metadata.srcElement.videoHeight. You can use those dimensions to size the stream video element if you want. If not, the video element will grow or shrink to accomodate the streaming video dimensions.

Catching failure

The failure callback—which might get called if the user rejects the stream or the contraints didn't match or some other error—should show an error message so the user isn't confused why nothing is happening. We can just log an error to the console for now.

function failureCallback() {
    console.error('Stream not available');
}

Putting it altogether

Before asking to stream, you can check to make sure that the user's browser has the ability to connect to media devices. You can check that by seeing if navigator.mediaDevices and navigator.mediaDevices.getUserMedia are both true. If they're not available, we can call the failureCallback function to log the error.

So we can wrap getUserMedia() in an if block, and put the whole thing in a function called startStreaming():


const startStreaming = () => {
    if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
        navigator.mediaDevices.getUserMedia(constraints)
        .then(sucessCallback)
        .catch(failureCallback);
    }
    else {
        failureCallback();
    }
}

Finally, we need a button for the user to press to trigger the stream.


    <button id="startStreamButton">Stream</button>

Then we can add an event listener to trigger the startStreaming function when it is pressed.

document.getElementById('startStreamButton')
.addEventListener('click', () => {
        startStreaming();
   });

That's all it takes to stream user video on the web!

Try streaming yourself!

If you're interested, the full code for streaming a user's web camera is below.
function successCallback(stream) {
    const streamElement = document.getElementById('videoStream');
    streamElement.srcObject = stream;
    streamElement.addEventListener('loadedmetadata', (metadata) => {
        streamElement.play();
    });
}

function failureCallback() {
    console.error('Stream not available');
}

const startStreaming = () => {
    if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
        const constraints = {
            audio: true,
            video: {
            width: 1280,
            height: 720,
            facingMode: "user",
            }
        }
        navigator.mediaDevices.getUserMedia(constraints)
        .then(successCallback)
        .catch(failureCallback);
    }
    else {
        failureCallback();
    }
}
document.getElementById('startStreamButton').addEventListener('click' () => {
    startStreaming();
});