Vue.js Composition API: usage with MediaDevices API
Tags: vue, javascript, tutorial
Published: 2019-12-30 22:18:02

## Introduction

In this article I would like to share my experience about how Vue Composition API helped me to organise and structure work with browser's [`navigator.mediaDevices`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/mediaDevices) API.

It's **highly encouraged** to skim through the RFC of the upcoming [Composition API](https://vue-composition-api-rfc.netlify.com) before reading.

## Task

The task I received was not trivial:

* application should display all the connected cameras, microphones and speakers that user have;
* user should have the ability to switch between them (e.g. if user have two cameras he/she can choose which one is active);
* application should appropriately react when user connects or disconnects devices;
* the solution should be easily reusable, so developers can use it on any page.

## Solution

For now, the only one way to reuse logic across components was `mixins`. But they have their own nasty drawbacks, so I decided to give a chance to a new Composition API.

>  Vue Composition API allows easily share stateful logic across components.

Let's start with separation of concerns – create three appropriate hooks `useCamera`, `useMicrophone`, `useSpeaker`. Each hook encapsulates the logic related to the specific device kind.

Let's look at one of them — `useCamera`:

`useCamera.ts`:

```ts
import { ref, onMounted, onUnmounted } from '@vue/composition-api'

export function useCamera() {
  const camera = ref('')
  const cameras = ref<MediaDeviceInfo[]>([])

  function handler() {
    navigator.mediaDevices.enumerateDevices().then(devices => {
      const value = devices.filter(device => device.kind === 'videoinput')
      cameras.value = value

      if (cameras.value.length > 0) {
        camera.value = cameras.value[0].deviceId
      }
    })
  }

  onMounted(() => {
    if (navigator && navigator.mediaDevices) {
      navigator.mediaDevices.addEventListener('devicechange', handler)
      handler()
    }
  })

  onUnmounted(() => {
    if (navigator && navigator.mediaDevices) {
      navigator.mediaDevices.removeEventListener('devicechange', handler)
    }
  })

  return {
    camera,
    cameras,
  }
}

```

Here is some explanations:

First off create two variables:

* `camera`, which will store the `deviceId` of the active camera (remember that user can choose active device);
* `cameras`, which will contain the list of all connected cameras.

These variables is supposed to be consumed by the component, so we return them.

There is `handler` function which enumerates all the connected devices and `push`es only those with `kind === 'videoinput'` to the `cameras` array. The type of `cameras` variable is `MediaDeviceInfo[]`, here is the snippet from [`lib.dom.d.ts`](https://github.com/microsoft/TypeScript/blob/master/lib/lib.dom.d.ts#L10237) which declares that interface:

```ts
type MediaDeviceKind = "audioinput" | "audiooutput" | "videoinput";

/** The MediaDevicesInfo interface contains information that describes a single media input or output device. */
interface MediaDeviceInfo {
    readonly deviceId: string;
    readonly groupId: string;
    readonly kind: MediaDeviceKind;
    readonly label: string;
    toJSON(): any;
}
```

Composition API provides us with `onMounted` and `onUnmounted` hooks, which is the analog to the current Options API `mounted` and `destroyed` hooks. As you can see, we invoke our `handler` function in `onMounted` hook to get the list of cameras, when component mounts.

Since devices can be connected or disconnected during the runtime of the application, we have to synchronize our data model with actually connected devices. To accomplish that task we need to subscribe to [`devicechange`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/ondevicechange) event which fires either when new device connects or already connected device disconnects. Since we did subscription, **we need to not forget to unsubscribe from this event when component is completely destroyed** to not catch any nasty bugs.

We have all set up, now let's use our custom hook in a component.

`component.vue`:

```vue
<script lang="ts">
import { createComponent, computed, watch } from '@vue/composition-api'
import { useCamera } from '@/use/camera'

export default createComponent({
  name: 'MyComponent',
  setup() {
    const { camera, cameras } = useCamera()

    const camerasLabels = computed(() =>
      cameras.value.map(camera => camera.label || camera.deviceId)
    )

    watch(cameras, value => {
      console.log(value)
    })

    return {
      camerasLabels,
    }
  },
})
</script>

<template>
  <section>Connected cameras: {{ camerasLabels }}</section>
</template>

```

Our hook can only be used during the invocation of a `setup` hook. When hook is invoked it returns our two variables: `camera` and `cameras`.

From that moment we can do whatever we want – we have fully reactive variables, as we would have with `data` using Options API.

For example, let's create a computed property `camerasLabels` which will list labels of `cameras`.

**Note** that when new camera connects or already connected camera disconnects our hook will handle it and update `cameras` value, which itself is reactive, so our template will be updated too. We can even watch for `cameras` and perform our custom logic.

The code of `useMicrophone` and `useSpeaker` code is the same, but the only difference is `device.kind` in the `handler` function. Thus, the solution can be reduced into the one hook – `useDevice`, which can accept the device kind as it's first argument:

```ts
export function useDevice(kind: MediaDeviceKind) {
  // ... the same logic
  function handler() {
    navigator.mediaDevices.enumerateDevices().then(devices => {
      const value = devices.filter(device => device.kind === kind) // <- filter by device kind
      // ... the same logic
    })
  }
  // ... the same logic
}
```

But I would prefer to split it up using three different hooks, because there might be logic specific to the device kind.

So our final solution looks something like this:

```ts
<script lang="ts">
import { createComponent, computed, watch } from '@vue/composition-api'

import { useCamera } from '../use/camera'
import { useMicrophone } from '../use/microphone'
import { useSpeaker } from '../use/speaker'

export default createComponent({
  name: 'App',
  setup() {
    const { camera, cameras } = useCamera()
    const { microphone, microphones } = useMicrophone()
    const { speaker, speakers } = useSpeaker()

    // computed
    const camerasLabels = computed(() =>
      cameras.value.map(camera => camera.label)
    )

    // or method
    function getDevicesLabels(devices: MediaDeviceInfo[]) {
      return devices.map(device => device.label)
    }

    watch(cameras, value => {
      console.log(value)
    })

    return {
      camerasLabels,
      microphones,
      speakers,
      getDevicesLabels,
    }
  },
})
</script>

<template>
  <ul>
    <li>Connected cameras: {{ camerasLabels }}</li>
    <li>Connected microphones: {{ getDevicesLabels(microphones) }}</li>
    <li>Connected speakers: {{ getDevicesLabels(speakers) }}</li>
  </ul>
</template> 
```

## Demo

Live demo is located [here](https://codesandbox.io/s/vue-composition-api-media-devices-ekz22). You can experiment with it a little – connect a new microphone or camera and you will see how the application reacts.

I have cheated a little. As you can see there are some lines:

```ts
await navigator.mediaDevices.getUserMedia({ video: true }) // <- in useCamera
await navigator.mediaDevices.getUserMedia({ audio: true }) // <- in useMicrophone and useSpeaker
```

It ensures that user have granted access to camera and microphone. If user have denied access to devices hooks won't work. So they implies that user have granted access to devices.

## Conclusion

We have created a bunch of useful hooks, that can be easily shared across projects to facilitate work with `navigator.mediaDevices`. Our hooks react to the actual devices state and synchronize it with data model. The API is simple enough – just execute hook in the `setup` method, all the logic is encapsulated in hook itself.

**P.S.** If you like the article, please, click "heart" or "unicorn" — it will give me some motivation to write the next article, where I plan to showcase how to combine these hooks in the `useMediaStream` hook, which contains stream with our active camera and microphone. Article also will describe how to change input and output sources of the stream.