How to take selfies with Javascript π½πΈ
Nonso Martin
Posted on April 29, 2024
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
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
#snip into the directory
cd dev-to-med*; npm i
# starts vite
npm run dev
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>
<!-- ... -->
As for the styling, the only things you might find strange is this
/* ... */
html {
height: 100dvh;
/* ... */
& body {
height: inherit;
}
}
what you see there is called
nesting
, if you've usedscss
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
*/
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 = [];
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();
}
Let's explain
I assume the DOM elements are self explanatory, with the exception of the
anchor
element which is dynamically created. Followed byctx
short for Canvas Context and for this cause we will be using2D
.
Next in line isvideoChunks
which will be used to store all the chunks of data collected from the Mic and Webcam.
clearPhoto and capturePhoto
will be explained belowour variables
let streaming = false;
let isPhoto = true;
Granting access
this is made possible through thegetUserMedia
property ofNavigator.mediaDevices
and it returns a Promise , since the whole thing is an asynchronous operation which mightfail 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));
hey hey! what's going on ?
getUserMedia
accepts an argument known as constrain and for this we havevideo and audio
set totrue
giving us access to the webcam and Mic respectively.
getUserMedia
returns aPromise
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 theMediaStream
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 orphoto 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
capturing videos requires the use of
new MediaRecorder(MediaStream)
Object, theMediaRecorder
Object expects at least an argument of typeMediaStream
to function properlynext, we start listening for events on the
MediaRecorder
Object
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 thecaptureButton
for the first time and it callsrecorder.start()
.
if(streaming && !isPhoto){
recorder.start();
/* ... */
}
- the second and last events
dataavailable
andstop
fire off when we have a call torecorder.stop()
. Once the call is madechunks
of data are saved in thevideoChunks
Array (within thedataavailable
event), and the rest is aboutsaving and downloading
the video and as well cleaning up everything (against data leaks) π«£.Taking selfies
this is where
capturePhoto
,clearPhoto
andcanvas
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 thewidth
andheight
of thecanvas
to equal that of the data gotten from your webcam , usingvideo.videoWidth
andvideo.videoHeight
properties.- followed by a call to
drawImage(video, 0, 0, w, h)
onctx
which draws an image unto the canvas.- below that is a call to
.toDataURL("image/png")
which returns the image in the form of adata 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 thesrc=""
attribute of theimg
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 theanchor
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. πΈπ
Posted on April 29, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.