Accessing Available Devices in Browser via getUserMedia()

I'll find something good to put here.
Search for a command to run...

I'll find something good to put here.
Update: In the handleSuccess callback I added a few lines stop the stream. It was a little disconcerting navigating to a new page and having the camera light still on.
stream.getTracks().forEach(function (track) {
track.stop(); // e.g. turn camera off, etc
});
In a future version, I'll store the device stream in a ref and run similar code when unmounting the React component.
In this series, I will be building some cool things with WebRTC and documenting as I go. Series Image Credit: Photo by Greg Gulik from Pexels
A quick code snippet to hide button text on small screens using MUI and TypeScript

A look at 7 different TypeScript scenarios requiring dependent and conditional typing

When I pitched this article to my graphic designer wife, she looked at me like I was speaking in tongues. However, I can't think of a more concise title. What this article will ultimately discuss is how something as seemingly non-quantitative as colo...

Refs in React have gone through several changes since their introduction. With the release of hooks in React 16.8, useRef became a handy way to interact with refs, especially in functional components. However, as TypeScript has come to dominate the J...

The Covid-19 pandemic got me scrambling to come up with some web based solutions to support local Summer art festivals. In April I started digging into WebRTC as a potential in-browser low-barrier-to entry Real Time video solution. After some successful proof of concepts, I realized browser support and all the bits necessary to bring my idea to life were going to require me to learn a lot more than I knew and dedicate a lot more time than I had. I ended up hacking together a wrapper on top of the Jitsi platform and it worked well enough to help out some artists. However, I am now back on a quest to explore WebRTC more fully.
In this post, I dive into some basics and ultimately demonstrate how to generate a list of available hardware devices for a User. I put together a demo on my site which is also a handy tool to help diagnose browser access to devices.
First we need to identify some objects and types we'll be working with. Note: That this is all available natively in the browser but some of the Types are from TypeScript.
navigator (docs) A global of type Navigator containing info about the application running the script.navigator.mediaDevices (docs) - a property of type MediaDevices which provides access to connected media devices like cameras, microphones, headsets, etc, as well as screen sharing. MediaConstraints (docs) An object containing information about what devices we want to access and requirements for these devices. This object can define, for example, that we prefer to access the front facing camera or a known audio input.navigator.mediaDevices.getUserMedia() (docs) A function that returns a Promise<MediaStream> containing information about the devices based on MediaConstraints argument. When executed, this will prompt the user to Allow or Block access to the particular kind of devices requested (eg. camera or microphone). Note: If the user has previously Blocked access to a particular device kind, this may skip the prompt and eject the promise immediately .navigator.mediaDevices.enumerateDevices() (docs) A function that returns a Promise<MediaDeviceInfo[]>. In Chrome, if the user has not yet Allowed access for the device kind, this may still resolve, but not contain complete information (such as missing device label). In Firefox, this will resolve with complete info even if the user Blocked access.MediaDeviceKind (docs) An enum of values for MediaDeviceInfo.kind. Allowed values are audioinput, audiooutput, and videoinputBelow is a basic example of requesting access to user's available microphones and cameras and logging out the available devices. Note: As a security measure, the user must interact with the page prior to calling getUserMedia or else it will error. As such, this is wrapped in an init() function.
// Enum of available MediaDeviceInfo.kind values
enum MediaDeviceKind {
CAMERA = 'videoinput',
MICROPHONE = 'audioinput',
SPEAKER = 'audiooutput',
}
// Helper Method to log out devices
const getAvailableDevices = () => {
// NOTE: Even if access is denied, there may be records depending on browser
let enumeratorPromise = navigator.mediaDevices.enumerateDevices();
enumeratorPromise.then((devices: MediaDeviceInfo[]) => {
console.log(devices);
});
};
const init = () => {
// Get User Media status for Audio
navigator.mediaDevices
.getUserMedia({ audio: true, video: false }) // MediaConstraints
.then(handleSuccess(MediaDeviceKind.MICROPHONE))
.catch(handleError(MediaDeviceKind.MICROPHONE))
.finally(() => {
getAvailableDevices();
});
// Get User Media status for Video
navigator.mediaDevices
.getUserMedia({ audio: false, video: true }) // MediaConstraints
.then(handleSuccess(MediaDeviceKind.CAMERA))
.catch(handleError(MediaDeviceKind.CAMERA))
.finally(() => {
getAvailableDevices();
});
// Finally Update state with all media devices.
getAvailableDevices();
};
// getUserMedia Promise callbacks
function handleSuccess(deviceType: MediaDeviceKind) {
return (stream: MediaStream) => {
console.log([deviceType, stream]);
// For this Demo, we don't actually do anything with the stream and want to release the device
// Note: This will cause the camera to momentarily blink
stream.getTracks().forEach(function (track) {
track.stop(); // e.g. turn camera off, etc
});
};
}
function handleError(deviceType: MediaDeviceKind) {
return (error: DOMException) => {
// An error occurred getting one or more media based on constraint.
let errMsg =
'Encountered Error while attempting to getUserMedia for ' + deviceType + ' ' + error.message + ' ' + error.name;
console.error(errMsg)
};
}
Notes:
MediaDeviceKind is an existing TypeScript type, I redefined as an enum so it can be used as a value in the code.getUserMedia separately for the camera and for the microphone. This was partially to more easily keep track of which device succeeded or failed, but also demonstrates that you can call getUserMedia only for what you need to access when you need to access it.getUserMedia handlers to return the actual callback function so it was easier to determine what device kind succeeded or failed. I put together a code lab to serve as a proof of concept as well as an easy way to debug different devices across browsers.
Once clicking "Start", to initialize the UI, you are prompted for access to the camera and mic.

If access is granted to both the mic and camera, the devices will be displayed:

If you reject either the camera or the mic, the respective getUserMedia promise rejects:

If I plug in my external USB webcam and refresh, the device will appear in the list as well:

Finally, if I plug in my Focusrite Scarlett audio interface, I see it as a microphone and speaker (see note below about Chrome and speakers):

Awesome!
There are some minor differences between browsers worth noting:
audiooutput. Only Chrome seems to keep track of that. Support for targeting output devices seems spotty.While functionality varies between browsers, it is fairly easy to get a list of hardware devices for a user as long as they fall under the category of "microphone" or "camera". This was a pretty easy starting point for digging into WebRTC. In the next post in this series, I will be leveraging these different devices to locally record audio and video and do some basic manipulations.
p.s. I'm really excited about this type of work. If you have a project working with WebRTC, especially if it is helping make the world better right now, hit me up.
Image Credit: Photo by pascal claivaz from Pexels