Build a Photo Booth App in JavaScript
By: Matthew Croak
Tags: Programming, JavaScript, CSS, HTML, Nodejs

HTML

There are a few buttons in this app. The first one is capture , which is responsible for prompting the user to give access to their webcam and then initializing the video stream. screenshot executes the function that takes a screenshot of the video and (using canvas ) will draw the screenshot and add it to the img tag’s src as a data URL.

download will capture the image data, create an anchor tag, assign the img.src to the anchor’s href , name the file and extension and then simulate a click event triggering the download. The clear button triggers the function that resets the UI and ends the video stream.

This brings us to the next part of the app: how do we actually start the video stream? Introducing the MediaStream interface.

The MediaStream interface represents a stream of media content, which consists of data broken up into tracks. These tracks are instances of the MediaStreamTrack which represents a single media track within a stream. In our case, we will be working with video tracks. Below is what one of these tracks looks like.

These tracks are where we will access the data in our video stream. We can capture this data by using navigator.mediaDevices.getUserMedia() . Below is an example.

navigator.mediaDevices.getUserMedia({video: true})

.then(stream => {console.log('we got the stream!')})

.catch(err=>alert('Error occurred: ' + err));

This function prompts the user to authorize access to the device’s camera. Upon authorization, the Promise created by getUserMedia will be either resolved (which will return the tracks) or it will be rejected, which is handled in the catch . You can read more about why getUserMedia might error out here.

Before we get into the workflow, here are the variables that we will be initializing for use throughout the app.

img is our img tag who’s src that will be assigned the video frame when the user clicks the screenshot button.

video is our video tag that will contain our video stream.

prompt1 and prompt2 are prompts that tell the user what the next steps are. If the user hasn’t started the video capture, prompt1 will be visibly telling them to do so. If they’ve started the capture, prompt2 will prompt them to take a screenshot.

screenshot is the button that takes a screenshot of the video stream.

download is the button that will trigger the image download.

select is our select tag that contains our file format options. This, along with label , representing the select ’s label, will be hidden until the user has taken a screenshot.

clear is the button that resets the UI and stops the stream.

enable will determine whether or not the app is usable on the device.

canvas is the canvas element we will be creating and using to write the video stream data to an image.

Finally, format is a file format selected by the user for the image download.

We can initialize our app by first checking if the user has getUserMedia with the function hasGetUserMedia .

const hasGetUserMedia = () => {

if (!navigator.mediaDevices &&

!navigator.mediaDevices.getUserMedia){

alert('Unable to enable camera.')

} else {

enable = true

}

}

This function will check if the user’s device has mediaDevices and getUserMedia . If it does, we set enable to true. When true, the rest of the app’s features can be used and we can start grabbing our DOM elements (which will be stored in their respective variables mentioned above) and then adding our event listeners to them. The first event listener is a onclick function called onCapture.

const onCapture = () => {

navigator.mediaDevices.getUserMedia({video: true})

.then(stream => {

video.srcObject = stream

prompt1.style.display = 'none';

prompt2.style.display = 'block';

screenshot.disabled = false;

clear.disabled = false;

})

.catch(err=>alert('Error occurred: ' + err));

}

By clicking the capture button, the user can start the media stream. This executes the getUserMedia function that will allow us to capture the user’s video stream. We assign that video stream to video ’s srcObject attribute. srcObject operates similarly for video as src does except instead of a static video file it allows the video element to use a MediaStream as its source.

At this point, we should now see our webcam’s live video on the left side of the application.

Now that we have the video stream working, we can move on to the next function, onScreenshot .

const onScreenshot = () => {

canvas.width = video.videoWidth;

canvas.height = video.videoHeight;

canvas.getContext('2d').drawImage(video, 0, 0);

prompt2.style.display = 'none'

img.src = canvas.toDataURL('image/png');

img.style.display = 'block'

download.disabled = false

select.style.visibility = 'visible'

label.style.visibility = 'visible'

}

If you are not already familiar, a canvas element can be used to draw graphics onto a webpage. In our case, we will be drawing an image. This is done by making use of drawImage . We can access this function by accessing a two-dimensional drawing context for canvas .

I’ve used this gif before. I am unashamed.

We pass drawImage three arguments. The first is our video element. drawImage will capture the frame from the video at that instance and draw it onto the canvas element. The next two arguments are the x and y coordinates on the canvas where we will place our drawing.

We use 0 for both, since we want the image to take up the entire canvas. That way it’s as close to the video’s ratio as possible since we set the canvas’ height and width equal to that of video . Then we use another canvas function, called toDataURL which will return a data URL that allows us to embed an image file inline. The rest of the function conditionally displays or hides other elements on the DOM to move the UX along.

At this point, we now have a running video stream and a screenshot drawn onto our canvas . The next part will involve allowing the user to download the generated image. There is a perfect example of how to do that here.

We create anchor tag, assign the href of that tag the value of our img ’s src tag, and then assign its download attribute the name yourScreenshot along with the chosen format . We append the anchor tag to the body of the DOM, simulate the user clicking it by using click() , and then remove the element from the DOM after it’s been used.

const onDownload = () => {

var download = document.createElement('a');

download.href = img.src

download.download = 'yourScreenshot' + format;

download.style.display = 'none';

document.body.appendChild(download);

download.click();

document.body.removeChild(download);

};

The user can now initialize a video stream, take a screenshot of the stream, and download that screenshot in a format of their choosing. This brings us to the last part of our application: allowing the user to end the video stream and reset the UI.

const clearAll = () => {

video.srcObject.getVideoTracks().forEach(track => track.stop())

document.getElementsByClassName('container')[0].removeChild(video);

video = document.createElement("video")

video.autoplay = true

document.getElementsByClassName('container')[0].insertBefore(video, prompt1)

if (img){

img.style.display = 'none'

}

screenshot.disabled = true;

download.disabled = true;

select.style.visibility = 'hidden';

label.style.display = 'none';

format = null;

prompt2.style.display = 'none';

prompt1.style.display = 'block';

clear.disabled = true;

}

A lot of this function is either hiding or showing different elements based on the initial UI requirements of the app, but there are two important parts.

video.srcObject.getVideoTracks().forEach(track => track.stop())

This line is crucial as it ends the user’s current media stream. We access the function getVideoTracks() via the video ’s srcObject attribute. This function returns an array of MediaStreamTracks that we can then iterate over and stop each track in its… tracks.

Whatever, Michelle, you’re like 3.

The next important part is removing the video element, creating a new one, and then inserting it back into the DOM in its original place. Without this, the video won’t reset to how it was initialized now and it will render a black square where the video should be playing.

Gross.

We can remove the video tag by using removeChild(video) on our app’s container. Then we can use document.createElement to create a new video tag. We assign its attribute autoplay to true so that once the video stream is picked up, the video will start/display in real-time. Finally, we use insertBefore and pass it the new video tag as the first argument and prompt1 as the second argument (since the original video tag comes just before prompt1 in the app container.

Future features include allowing the user to add a background and filters.

Thanks for reading! The source code for this app can be found in the GitHub repo.