How to take selfies with Javascript 😽📸
Tags: webdev, javascript, beginners, html
Published: 2024-04-29 11:54:36


cover image by [Laura Lee Moreau](https://unsplash.com/photos/closeup-photo-of-person-carrying-professional-video-recorder-ZpE2gobovro?utm_content=creditShareLink&utm_medium=referral&utm_source=unsplash)
> 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](https://webrtc.org)** (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**

```sh
# Hope you have nodejs ?
node -v
```
> if nodejs is missing do well to download it, for more info visit 👉🏽 [Nodejs Website](https://nodejs.org)

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

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

>  ooo! yes, plain `html, css and js` 😌
```html
<!-- ... -->

<!-- 
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>

<!-- ... -->
```
> As for the styling, the only things you might find strange is this 
```css
/* ... */
html {
height: 100dvh;
/* ... */
& body {
height: inherit;
 }
}
```

> 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**

```js
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
*/
```

> **our constants**
```js
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 = [];

```
> **useful functions**

```js
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();
}
```

> **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**

```js
let streaming = false;
let isPhoto = true;
```

> **Granting access**
> this is made possible through the **`getUserMedia`** property of **`Navigator.mediaDevices`** and it returns a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) , since the whole thing is an asynchronous operation which might **`fail or not`**

```js
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));
```
> **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
```js
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
```

> - 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

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

/* similar to
* recorder.onstart = e => /* ... */
*/
```

> - the first event of the **`MediaRecorder`**, **`start`** fires off when we click on the `captureButton` for the first time and it calls **`recorder.start()`**.

```js
if(streaming && !isPhoto){
recorder.start();
/* ... */
}
```
> - 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. 📸😎