How to take selfies with Javascript πŸ˜½πŸ“Έ

nonso01

Nonso Martin

Posted on April 29, 2024

How to take selfies with Javascript πŸ˜½πŸ“Έ

cover image by Laura Lee Moreau

This is my very first article on dev.to, and it was made for anyone just debuting with Javascript, hope it's worth your time.

Goal

the goal of this project is for you to be able to know how to use the MediaStream API when need be, or to simply take selfies and clips of yourself from your browser

Let's get started

Before we define any word, or write a line of code, i strongly suggest you grant your web browser access to your Microphone 🎀 and Webcam πŸ“Ή (for the sake of this project), without these permissions you won't be able to see or do anything πŸ₯².

What is the Media capture and Streams API ?

it's an API which provides support or methods for transmitting and receiving data, especially audio and video data. Which is related to WebRTC (web real time communication). This are simply the tools which allows one to capture videos, audios and as well perform real time communications (endless video calls with your loved ones 😽)

the API is based on the manipulation of a MediaStream Object, which contains 0 or more data tracks to manipulate.

Time for some spaghetti code

# Hope you have nodejs ?
node -v
Enter fullscreen mode Exit fullscreen mode

if nodejs is missing do well to download it, for more info visit πŸ‘‰πŸ½ Nodejs Website

Next clone into the repo containing the whole project from github.

git clone https://github.com/nonso01/dev-to-media-stream-article.git
Enter fullscreen mode Exit fullscreen mode
#snip into the directory
cd dev-to-med*; npm i
Enter fullscreen mode Exit fullscreen mode
# starts vite 
npm run dev
Enter fullscreen mode Exit fullscreen mode

ooo! yes, plain html, css and js 😌

<!-- ... -->

<!-- 
all the elements below are structured accordingly
only these 5 elements are of importance
 -->

<video></video>
<img alt="your-cute-image" /> <!-- no src attr -->
<canvas></canvas>

<button class="capture"></button>
<button class="photo"></button>
<button class="video"></button>

<!-- ... -->
Enter fullscreen mode Exit fullscreen mode

As for the styling, the only things you might find strange is this

/* ... */
html {
height: 100dvh;
/* ... */
& body {
height: inherit;
 }
}
Enter fullscreen mode Exit fullscreen mode

what you see there is called nesting, if you've used scss before then you should be familiar with it. Hopefully it's now supported in native css, so yeah 😌, css has super powers now. And some animations and transitions were added for aesthetics.

Unto the fun part

import On fom "on-dom"
import "./style.css"
/*
* "on-dom" or On is a special function(class) i 
* created, which allows the user to attach multiple 
* events all at once to an Object, NodeList, or DOMElement.
* to know more visit https://www.npmjs.com/package/on-dom
*/
Enter fullscreen mode Exit fullscreen mode

our constants

const log = console.log;
const ONESEC = 1e3;

const captureButton = select(".capture"); 
const videoButton = select("button.video");
const photoButton = select("button.photo");
const canvas = select("canvas");
const video = select("video"); 
const photo = select(".app-media img");
const link = document.createElement("a");

const ctx = canvas.getContext("2d");

const videoChunks = [];

Enter fullscreen mode Exit fullscreen mode

useful functions

function select(str = "html") {
return document.querySelector(str);
}

function clearPhoto() {
  canvas.width = video.videoWidth;
  canvas.height = video.videoHeight;

  ctx.fillStyle = "#222";
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  const data = canvas.toDataURL("image/png");
  photo.setAttribute("src", data);
}

function capturePhoto(w = video.videoWidth, h = video.videoHeight) {
  if (w && h) {
    canvas.width = w;
    canvas.height = h;
    ctx.drawImage(video, 0, 0, w, h);
    const data = canvas.toDataURL("image/png");
    photo.setAttribute("src", data);

    fetch(data, { mode: "no-cors" }) /* download your image */
      .then((res) => res.blob())
      .then((blob) => {
        const photoLink = URL.createObjectURL(blob);
        link.href = photoLink;
        link.download = `image-${Math.floor(Math.random() * 250)}.png`;
        link.click();

        URL.revokeObjectURL(photoLink);
      })
      .catch((err) => console.warn(err?.message));
  } else clearPhoto();
}
Enter fullscreen mode Exit fullscreen mode

Let's explain

I assume the DOM elements are self explanatory, with the exception of the anchor element which is dynamically created. Followed by ctx short for Canvas Context and for this cause we will be using 2D.
Next in line is videoChunks which will be used to store all the chunks of data collected from the Mic and Webcam.

clearPhoto and capturePhoto will be explained below

our variables

let streaming = false;
let isPhoto = true;
Enter fullscreen mode Exit fullscreen mode

Granting access
this is made possible through the getUserMedia property of Navigator.mediaDevices and it returns a Promise , since the whole thing is an asynchronous operation which might fail or not

const grantAccess = navigator.mediaDevices
  .getUserMedia({
    video: true,
    audio: true,
  })
  .then((stream /* MediaStream */) => {
    const recorder = new MediaRecorder(stream, {
      mimeType: "video/webm; codecs=vp8,opus",
    });

    video.srcObject = stream;
    video.play();

    const recorderEvents = new On(recorder, {
      start: (e) => log("video started"),
      dataavailable: (e) => videoChunks.push(e.data),
      stop() {
        const blob = new Blob(videoChunks, { type: "video/webm" });
        const videoLink = URL.createObjectURL(blob);
        link.href = videoLink;
        link.download = `video-${Math.floor(Math.random() * 255)}.webm`;
        link.click();

        videoChunks.length = 0; // clean up
        URL.revokeObjectURL(videoLink); // against data leaks
      },
      error: (e) => log(e),
    });

    const captureEvents = new On(captureButton, {
      pointerover() {
        this.classList.add("over");
      },
      pointerleave() {
        this.classList.remove("over");
      },
      click() {
        streaming = !streaming;
        if (streaming && !isPhoto) {
          recorder.start();
          clearPhoto();

          appBeep.classList.add("beep");
        } else if (!streaming && !isPhoto) {
          recorder.stop();
          clearPhoto();

          appBeep.classList?.remove("beep");
          // stream.getTracks().forEach((track) => track.stop());
          log("video ended");
        } else if (!streaming && isPhoto) {
          capturePhoto();

          // minimal animation
          photo.classList.add("captured");
          photo.ontransitionend = () =>
            setTimeout(() => {
              photo.classList?.remove("captured");
            }, ONESEC * 1.5);
        }
      },
    });

    videoButton.onclick = (e) => {
      isPhoto = false;
      clearPhoto();

      appType.textContent = "πŸŽ₯";
      captureButton.classList.add("is-video");
    };

    photoButton.onclick = (e) => {
      appType.textContent = "πŸ“·";
      recorder.stop();

      isPhoto = true;
      streaming = false;

      captureButton.classList.contains("is-video")
        ? captureButton.classList.remove("is-video")
        : void 0;
      appBeep.classList.remove("beep");
    };
  })
  .catch((err) => console.warn(err));
Enter fullscreen mode Exit fullscreen mode

hey hey! what's going on ?

  • getUserMedia accepts an argument known as constrain and for this we have video and audio set to true giving us access to the webcam and Mic respectively.

  • getUserMedia returns a Promise and we will need to handle it , using one of its methods and that will be .then() and since we have access to the device , we will be able to manipulate the MediaStream Object, which is seen as an argument to the call to .then(stream => {...})

Taking video clips

if you downloaded the project, click on video button, you'll see a green ring appear, then click on the round button, to stop recording, click on either the round button again or photo button.

  • looking at the code above, you'll see
video.srcObject = stream;
/*
* unlike the src attr, srcObject points to MediaSource, MediaStream, Blob or File
*/
video.play(); // play as soon as the source is available
Enter fullscreen mode Exit fullscreen mode
  • capturing videos requires the use of

    • new MediaRecorder(MediaStream) Object, the MediaRecorder Object expects at least an argument of type MediaStream to function properly
  • next, we start listening for events on the MediaRecorder Object

const recorderEvents = new On(recorder, {
start: () => /* ... */,
dataavailable: e => /* ... */,
stop: () => /* ... */
});

/* similar to
* recorder.onstart = e => /* ... */
*/
Enter fullscreen mode Exit fullscreen mode
  • the first event of the MediaRecorder, start fires off when we click on the captureButton for the first time and it calls recorder.start().
if(streaming && !isPhoto){
recorder.start();
/* ... */
}
Enter fullscreen mode Exit fullscreen mode
  • the second and last events dataavailable and stop fire off when we have a call to recorder.stop(). Once the call is made chunks of data are saved in the videoChunks Array (within the dataavailable event), and the rest is about saving and downloading the video and as well cleaning up everything (against data leaks) 🫣.

Taking selfies

this is where capturePhoto, clearPhoto and canvas comes into play.

  • the principal focus here, about capturing pictures is the canvas element and its various methods.
    • within the capturePhoto(w, h) function, we set the width and height of the canvas to equal that of the data gotten from your webcam , using video.videoWidth and video.videoHeight properties.
    • followed by a call to drawImage(video, 0, 0, w, h) on ctx which draws an image unto the canvas.
    • below that is a call to .toDataURL("image/png") which returns the image in the form of a data URL, we all know images are a bunch of encoded data in the form of a giant string, you might try opening your dev tool, while having a careful look at the src="" attribute of the img element after you must have captured an image.
    • then feed the src attribute with your data 🫣

Downloading your media

i made use of Blob, URL, fetch and the anchor for downloading the data collected. i would explain the above Objects in the next article.

Conclusion

so much can be achieved, with the MediaStream API like ;

  • creating chat apps, with video capturing features
  • Video games
  • VR contents
  • AI projects
  • And More

Hope this helps, Have a nice day. πŸ“ΈπŸ˜Ž

πŸ’– πŸ’ͺ πŸ™… 🚩
nonso01
Nonso Martin

Posted on April 29, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related