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
MediaStreamObject, 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 usedscssbefore then you should be familiar with it. Hopefully it's now supported in native css, so yeah π,csshas 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
anchorelement which is dynamically created. Followed byctxshort for Canvas Context and for this cause we will be using2D.
Next in line isvideoChunkswhich will be used to store all the chunks of data collected from the Mic and Webcam.
clearPhoto and capturePhotowill be explained belowour variables
let streaming = false;
let isPhoto = true;
Granting access
this is made possible through thegetUserMediaproperty ofNavigator.mediaDevicesand 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 ?
getUserMediaaccepts an argument known as constrain and for this we havevideo and audioset totruegiving us access to the webcam and Mic respectively.
getUserMediareturns aPromiseand 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 theMediaStreamObject, 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, theMediaRecorderObject expects at least an argument of typeMediaStreamto function properlynext, we start listening for events on the
MediaRecorderObject
const recorderEvents = new On(recorder, {
start: () => /* ... */,
dataavailable: e => /* ... */,
stop: () => /* ... */
});
/* similar to
* recorder.onstart = e => /* ... */
*/
- the first event of the
MediaRecorder,startfires off when we click on thecaptureButtonfor the first time and it callsrecorder.start().
if(streaming && !isPhoto){
recorder.start();
/* ... */
}
- the second and last events
dataavailableandstopfire off when we have a call torecorder.stop(). Once the call is madechunksof data are saved in thevideoChunksArray (within thedataavailableevent), and the rest is aboutsaving and downloadingthe video and as well cleaning up everything (against data leaks) π«£.Taking selfies
this is where
capturePhoto,clearPhotoandcanvascomes into play.
- the principal focus here, about capturing pictures is the
canvaselement and its various methods.
- within the
capturePhoto(w, h)function, we set thewidthandheightof thecanvasto equal that of the data gotten from your webcam , usingvideo.videoWidthandvideo.videoHeightproperties.- followed by a call to
drawImage(video, 0, 0, w, h)onctxwhich 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 theimgelement after you must have captured an image.- then feed the
srcattribute with your data π«£Downloading your media
i made use of
Blob,URL,fetchand theanchorfor downloading the data collected. i would explain the above Objects in the next article.Conclusion
so much can be achieved, with the
MediaStream APIlike ;
- creating chat apps, with video capturing features
- Video games
- VR contents
- AI projects
- And More
Hope this helps, Have a nice day. πΈπ
Top comments (0)