diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx
index 074290f..a66b713 100644
--- a/src/room/InCallView.tsx
+++ b/src/room/InCallView.tsx
@@ -63,7 +63,7 @@ import { ParticipantInfo } from "./useGroupCall";
 import { TileDescriptor } from "../video-grid/TileDescriptor";
 import { AudioSink } from "../video-grid/AudioSink";
 import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts";
-import { MediaDevicesState } from "./devices/useMediaDevices";
+import { MediaDevicesState } from "../settings/mediaDevices";
 
 const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
 // There is currently a bug in Safari our our code with cloning and sending MediaStreams
diff --git a/src/room/LobbyView.tsx b/src/room/LobbyView.tsx
index ce5f225..e58e5cb 100644
--- a/src/room/LobbyView.tsx
+++ b/src/room/LobbyView.tsx
@@ -26,7 +26,7 @@ import { UserMenuContainer } from "../UserMenuContainer";
 import { Body, Link } from "../typography/Typography";
 import { useLocationNavigation } from "../useLocationNavigation";
 import { LocalMediaInfo, MatrixInfo, VideoPreview } from "./VideoPreview";
-import { MediaDevicesState } from "./devices/useMediaDevices";
+import { MediaDevicesState } from "../settings/mediaDevices";
 
 interface Props {
   matrixInfo: MatrixInfo;
diff --git a/src/room/OverflowMenu.tsx b/src/room/OverflowMenu.tsx
index 0e0c3d9..01312e9 100644
--- a/src/room/OverflowMenu.tsx
+++ b/src/room/OverflowMenu.tsx
@@ -32,7 +32,7 @@ import { InviteModal } from "./InviteModal";
 import { TooltipTrigger } from "../Tooltip";
 import { FeedbackModal } from "./FeedbackModal";
 import { Config } from "../config/Config";
-import { MediaDevicesState } from "./devices/useMediaDevices";
+import { MediaDevicesState } from "../settings/mediaDevices";
 
 interface Props {
   roomId: string;
diff --git a/src/room/VideoPreview.tsx b/src/room/VideoPreview.tsx
index 8142418..29aa589 100644
--- a/src/room/VideoPreview.tsx
+++ b/src/room/VideoPreview.tsx
@@ -24,7 +24,7 @@ import { OverflowMenu } from "./OverflowMenu";
 import { Avatar } from "../Avatar";
 import styles from "./VideoPreview.module.css";
 import { useModalTriggerState } from "../Modal";
-import { MediaDevicesState } from "./devices/useMediaDevices";
+import { MediaDevicesState } from "../settings/mediaDevices";
 
 export type MatrixInfo = {
   userName: string;
@@ -36,7 +36,7 @@ export type MatrixInfo = {
 export type MediaInfo = {
   track: Track; // TODO: Replace it by a more generic `CallFeed` type from JS SDK once we generalise the types.
   muted: boolean;
-  setMuted: (muted: boolean) => void;
+  toggle: () => void;
 };
 
 export type LocalMediaInfo = {
@@ -67,7 +67,7 @@ export function VideoPreview({
     return () => {
       localMediaInfo.video?.track.detach();
     };
-  }, [localMediaInfo]);
+  }, [localMediaInfo.video?.track, mediaElement]);
 
   return (
     <div className={styles.preview} ref={previewRef}>
@@ -86,17 +86,13 @@ export function VideoPreview({
           {localMediaInfo.audio && (
             <MicButton
               muted={localMediaInfo.audio?.muted}
-              onPress={() =>
-                localMediaInfo.audio?.setMuted(!localMediaInfo.audio?.muted)
-              }
+              onPress={localMediaInfo.audio?.toggle}
             />
           )}
           {localMediaInfo.video && (
             <VideoButton
               muted={localMediaInfo.video?.muted}
-              onPress={() =>
-                localMediaInfo.video?.setMuted(!localMediaInfo.video?.muted)
-              }
+              onPress={localMediaInfo.video?.toggle}
             />
           )}
           <OverflowMenu
diff --git a/src/room/devices/mediaDevices.ts b/src/room/devices/mediaDevices.ts
deleted file mode 100644
index 49b1068..0000000
--- a/src/room/devices/mediaDevices.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import type TypedEmitter from "typed-emitter";
-
-/* This file should become a part of LiveKit JS SDK. */
-
-// Generic interface for all types that are capable of providing and managing media devices.
-export interface MediaDevicesManager
-  extends TypedEmitter<MediaDeviceHandlerCallbacks> {
-  getDevices(kind: MediaDeviceKind): Promise<MediaDeviceInfo[]>;
-  setActiveDevice(kind: MediaDeviceKind, deviceId: string): Promise<void>;
-}
-
-export type MediaDeviceHandlerCallbacks = {
-  devicesChanged: () => Promise<void>;
-};
-
-export enum MediaDeviceHandlerEvents {
-  DevicesChanged = "devicesChanged",
-}
diff --git a/src/room/devices/useMediaDevices.ts b/src/room/devices/useMediaDevices.ts
deleted file mode 100644
index 445bb4f..0000000
--- a/src/room/devices/useMediaDevices.ts
+++ /dev/null
@@ -1,149 +0,0 @@
-import { useEffect, useState } from "react";
-
-import { MediaDevicesManager } from "./mediaDevices";
-
-export type MediaDevices = {
-  available: MediaDeviceInfo[];
-  selected: number;
-};
-
-export type MediaDevicesState = {
-  state: Map<MediaDeviceKind, MediaDevices>;
-  selectActiveDevice: (
-    kind: MediaDeviceKind,
-    deviceId: string
-  ) => Promise<void>;
-};
-
-export function useMediaDevices(
-  mediaDeviceHandler: MediaDevicesManager
-): MediaDevicesState {
-  // Create a React state to store the available devices and the selected device for each kind.
-  const [state, setState] = useState<Map<MediaDeviceKind, MediaDevices>>(
-    new Map()
-  );
-
-  // Update the React state when the available devices change.
-  useEffect(() => {
-    // Define a callback that is going to be called each time the available devices change.
-    const updateDevices = async () => {
-      const mediaDeviceKinds: MediaDeviceKind[] = [
-        "audioinput",
-        "audiooutput",
-        "videoinput",
-      ];
-
-      const newState = new Map(state);
-
-      // Request all the available devices for each kind.
-      for (const kind of mediaDeviceKinds) {
-        const devices = await mediaDeviceHandler.getDevices(
-          kind as MediaDeviceKind
-        );
-
-        // If newly requested devices are empty, remove the kind from the React state.
-        if (devices.length === 0) {
-          newState.delete(kind);
-          continue;
-        }
-
-        // Otherwise, check if the current state contains any selected device and find this device in the new list of devices.
-        // If the device exists, update the React state with the new list of devices and the index of the selected device.
-        // If the device does not exist, select the first one (default device).
-        const selectedDevice = state.get(kind);
-        const newSelectedDeviceIndex = selectedDevice
-          ? devices.findIndex(
-              (device) =>
-                device.deviceId ===
-                selectedDevice.available[selectedDevice.selected].deviceId
-            )
-          : 0;
-
-        newState.set(kind, {
-          available: devices,
-          selected: newSelectedDeviceIndex !== -1 ? newSelectedDeviceIndex : 0,
-        });
-      }
-
-      if (devicesChanged(state, newState)) {
-        setState(newState);
-      }
-    };
-
-    updateDevices();
-
-    mediaDeviceHandler.on("devicesChanged", updateDevices);
-    return () => {
-      mediaDeviceHandler.off("devicesChanged", updateDevices);
-    };
-  }, [mediaDeviceHandler, state]);
-
-  const selectActiveDeviceFunc = async (
-    kind: MediaDeviceKind,
-    deviceId: string
-  ) => {
-    await mediaDeviceHandler.setActiveDevice(kind, deviceId);
-
-    // Update react state as well.
-    setState((prevState) => {
-      const newState = new Map(prevState);
-      const devices = newState.get(kind);
-      if (!devices) {
-        return newState;
-      }
-
-      const newSelectedDeviceIndex = devices.available.findIndex(
-        (device) => device.deviceId === deviceId
-      );
-
-      newState.set(kind, {
-        available: devices.available,
-        selected: newSelectedDeviceIndex,
-      });
-
-      return newState;
-    });
-  };
-
-  const [selectActiveDevice] = useState<
-    (kind: MediaDeviceKind, deviceId: string) => Promise<void>
-  >(selectActiveDeviceFunc);
-
-  return {
-    state,
-    selectActiveDevice,
-  };
-}
-
-// Determine if any devices changed between the old and new state.
-function devicesChanged(
-  map1: Map<MediaDeviceKind, MediaDevices>,
-  map2: Map<MediaDeviceKind, MediaDevices>
-): boolean {
-  if (map1.size !== map2.size) {
-    return true;
-  }
-
-  for (const [key, value] of map1) {
-    const newValue = map2.get(key);
-    if (!newValue) {
-      return true;
-    }
-
-    if (value.selected !== newValue.selected) {
-      return true;
-    }
-
-    if (value.available.length !== newValue.available.length) {
-      return true;
-    }
-
-    for (let i = 0; i < value.available.length; i++) {
-      if (value.available[i].deviceId !== newValue.available[i].deviceId) {
-        return true;
-      }
-    }
-  }
-
-  return false;
-}
diff --git a/src/room/useLiveKit.ts b/src/room/useLiveKit.ts
index 23b6eb8..46ba70f 100644
--- a/src/room/useLiveKit.ts
+++ b/src/room/useLiveKit.ts
@@ -1,16 +1,9 @@
-import { EventEmitter } from "events";
-import { Room, RoomEvent, Track } from "livekit-client";
+import { LocalAudioTrack, LocalVideoTrack, Room } from "livekit-client";
 import React from "react";
-import { useLocalParticipant } from "@livekit/components-react";
+import { useMediaDevices, usePreviewDevice } from "@livekit/components-react";
 
-import {
-  MediaDeviceHandlerCallbacks,
-  MediaDeviceHandlerEvents,
-  MediaDevicesManager,
-} from "./devices/mediaDevices";
-import { MediaDevicesState, useMediaDevices } from "./devices/useMediaDevices";
+import { MediaDevicesState, MediaDevices } from "../settings/mediaDevices";
 import { LocalMediaInfo, MediaInfo } from "./VideoPreview";
-import type TypedEmitter from "typed-emitter";
 
 type LiveKitState = {
   mediaDevices: MediaDevicesState;
@@ -33,32 +26,35 @@ export function useLiveKit(
     return new Room();
   });
 
-  const [mediaDevicesManager] = React.useState<MediaDevicesManager>(() => {
-    return new LkMediaDevicesManager(room);
-  });
+  // Create a React state to store the available devices and the selected device for each kind.
+  const mediaDevices = useMediaDevicesState(room);
 
-  const { state: mediaDevicesState, selectActiveDevice: selectDeviceFn } =
-    useMediaDevices(mediaDevicesManager);
+  // Create local video track.
+  const [videoEnabled, setVideoEnabled] = React.useState<boolean>(true);
+  const selectedVideoId = mediaDevices.state.get("videoinput")?.selectedId;
+  const video = usePreviewDevice(
+    videoEnabled,
+    selectedVideoId ?? "",
+    "videoinput"
+  );
 
-  React.useEffect(() => {
-    console.log("media devices changed, mediaDevices:", mediaDevicesState);
-  }, [mediaDevicesState]);
-
-  const {
-    microphoneTrack,
-    isMicrophoneEnabled,
-    cameraTrack,
-    isCameraEnabled,
-    localParticipant,
-  } = useLocalParticipant({ room });
+  // Create local audio track.
+  const [audioEnabled, setAudioEnabled] = React.useState<boolean>(true);
+  const selectedAudioId = mediaDevices.state.get("audioinput")?.selectedId;
+  const audio = usePreviewDevice(
+    audioEnabled,
+    selectedAudioId ?? "",
+    "audioinput"
+  );
 
+  // Create final LiveKit state.
   const [state, setState] = React.useState<LiveKitState | undefined>(undefined);
   React.useEffect(() => {
-    // Helper to create local media without the
+    // Helper to create local media without the copy-paste.
     const createLocalMedia = (
+      track: LocalVideoTrack | LocalAudioTrack | undefined,
       enabled: boolean,
-      track: Track | undefined,
-      setEnabled
+      setEnabled: React.Dispatch<React.SetStateAction<boolean>>
     ): MediaInfo | undefined => {
       if (!track) {
         return undefined;
@@ -67,29 +63,24 @@ export function useLiveKit(
       return {
         track,
         muted: !enabled,
-        setMuted: async (newState: boolean) => {
-          if (enabled != newState) {
-            await setEnabled(newState);
-          }
+        toggle: async () => {
+          setEnabled(!enabled);
         },
       };
     };
 
     const state: LiveKitState = {
-      mediaDevices: {
-        state: mediaDevicesState,
-        selectActiveDevice: selectDeviceFn,
-      },
+      mediaDevices: mediaDevices,
       localMedia: {
         audio: createLocalMedia(
-          isMicrophoneEnabled,
-          microphoneTrack?.track,
-          localParticipant.setMicrophoneEnabled
+          audio.localTrack,
+          audioEnabled,
+          setAudioEnabled
         ),
         video: createLocalMedia(
-          isCameraEnabled,
-          cameraTrack?.track,
-          localParticipant.setCameraEnabled
+          video.localTrack,
+          videoEnabled,
+          setVideoEnabled
         ),
       },
       enterRoom: async () => {
@@ -105,41 +96,188 @@ export function useLiveKit(
   }, [
     url,
     token,
+    mediaDevices,
+    audio.localTrack,
+    video.localTrack,
+    audioEnabled,
+    videoEnabled,
     room,
-    mediaDevicesState,
-    selectDeviceFn,
-    localParticipant,
-    microphoneTrack,
-    cameraTrack,
-    isMicrophoneEnabled,
-    isCameraEnabled,
   ]);
 
   return state;
 }
 
-// Implement the MediaDevicesHandler interface for the LiveKit's Room class by wrapping it, so that
-// we can pass the confined version of the `Room` to the `MediaDevicesHandler` consumers.
-export class LkMediaDevicesManager
-  extends (EventEmitter as new () => TypedEmitter<MediaDeviceHandlerCallbacks>)
-  implements MediaDevicesManager
-{
-  private room: Room;
+function useMediaDevicesState(room: Room): MediaDevicesState {
+  // Video input state.
+  const videoInputDevices = useMediaDevices({ kind: "videoinput" });
+  const [selectedVideoInput, setSelectedVideoInput] =
+    React.useState<string>("");
 
-  constructor(room: Room) {
-    super();
-    this.room = room;
+  // Audio input state.
+  const audioInputDevices = useMediaDevices({ kind: "audioinput" });
+  const [selectedAudioInput, setSelectedAudioInput] =
+    React.useState<string>("");
 
-    this.room.on(RoomEvent.MediaDevicesChanged, () => {
-      this.emit(MediaDeviceHandlerEvents.DevicesChanged);
+  // Audio output state.
+  const audioOutputDevices = useMediaDevices({ kind: "audiooutput" });
+  const [selectedAudioOut, setSelectedAudioOut] = React.useState<string>("");
+
+  // Install hooks, so that we react to changes in the available devices.
+  React.useEffect(() => {
+    // Helper type to make the code more readable.
+    type DeviceHookData = {
+      kind: MediaDeviceKind;
+      available: MediaDeviceInfo[];
+      selected: string;
+      setSelected: React.Dispatch<React.SetStateAction<string>>;
+    };
+
+    const videoInputHook: DeviceHookData = {
+      kind: "videoinput",
+      available: videoInputDevices,
+      selected: selectedVideoInput,
+      setSelected: setSelectedVideoInput,
+    };
+
+    const audioInputHook: DeviceHookData = {
+      kind: "audioinput",
+      available: audioInputDevices,
+      selected: selectedAudioInput,
+      setSelected: setSelectedAudioInput,
+    };
+
+    const audioOutputHook: DeviceHookData = {
+      kind: "audiooutput",
+      available: audioOutputDevices,
+      selected: selectedAudioOut,
+      setSelected: setSelectedAudioOut,
+    };
+
+    const updateDevice = async (kind: MediaDeviceKind, id: string) => {
+      try {
+        await room.switchActiveDevice(kind, id);
+      } catch (e) {
+        console.error("Failed to switch device", e);
+      }
+    };
+
+    for (const hook of [videoInputHook, audioInputHook, audioOutputHook]) {
+      if (hook.available.length === 0) {
+        const newSelected = "";
+        hook.setSelected(newSelected);
+        updateDevice(hook.kind, newSelected);
+        continue;
+      }
+
+      const found = hook.available.find(
+        (device) => device.deviceId === hook.selected
+      );
+
+      if (!found) {
+        const newSelected = hook.available[0].deviceId;
+        hook.setSelected(newSelected);
+        updateDevice(hook.kind, newSelected);
+        continue;
+      }
+    }
+  }, [
+    videoInputDevices,
+    selectedVideoInput,
+    audioInputDevices,
+    selectedAudioInput,
+    audioOutputDevices,
+    selectedAudioOut,
+    room,
+  ]);
+
+  const selectActiveDevice = async (kind: MediaDeviceKind, id: string) => {
+    switch (kind) {
+      case "audioinput":
+        setSelectedAudioInput(id);
+        break;
+      case "videoinput":
+        setSelectedVideoInput(id);
+        break;
+      case "audiooutput":
+        setSelectedAudioOut(id);
+        break;
+    }
+  };
+
+  const [mediaDevicesState, setMediaDevicesState] =
+    React.useState<MediaDevicesState>(() => {
+      const state: MediaDevicesState = {
+        state: new Map(),
+        selectActiveDevice,
+      };
+      return state;
     });
-  }
 
-  async getDevices(kind: MediaDeviceKind) {
-    return await Room.getLocalDevices(kind);
-  }
+  React.useEffect(() => {
+    // Fill the map of the devices with the current state.
+    const mediaDevices = new Map<MediaDeviceKind, MediaDevices>();
+    mediaDevices.set("audioinput", {
+      available: audioInputDevices,
+      selectedId: selectedAudioInput,
+    });
+    mediaDevices.set("videoinput", {
+      available: videoInputDevices,
+      selectedId: selectedVideoInput,
+    });
+    mediaDevices.set("audiooutput", {
+      available: audioOutputDevices,
+      selectedId: selectedAudioOut,
+    });
 
-  async setActiveDevice(kind: MediaDeviceKind, deviceId: string) {
-    await this.room.switchActiveDevice(kind, deviceId);
-  }
+    if (devicesChanged(mediaDevicesState.state, mediaDevices)) {
+      const newState: MediaDevicesState = {
+        state: mediaDevices,
+        selectActiveDevice,
+      };
+      setMediaDevicesState(newState);
+    }
+  }, [
+    audioInputDevices,
+    selectedAudioInput,
+    videoInputDevices,
+    selectedVideoInput,
+    audioOutputDevices,
+    selectedAudioOut,
+    mediaDevicesState.state,
+  ]);
+
+  return mediaDevicesState;
+}
+
+// Determine if any devices changed between the old and new state.
+function devicesChanged(
+  map1: Map<MediaDeviceKind, MediaDevices>,
+  map2: Map<MediaDeviceKind, MediaDevices>
+): boolean {
+  if (map1.size !== map2.size) {
+    return true;
+  }
+
+  for (const [key, value] of map1) {
+    const newValue = map2.get(key);
+    if (!newValue) {
+      return true;
+    }
+
+    if (value.selectedId !== newValue.selectedId) {
+      return true;
+    }
+
+    if (value.available.length !== newValue.available.length) {
+      return true;
+    }
+
+    for (let i = 0; i < value.available.length; i++) {
+      if (value.available[i].deviceId !== newValue.available[i].deviceId) {
+        return true;
+      }
+    }
+  }
+
+  return false;
 }
diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx
index 6d44963..1dc9524 100644
--- a/src/settings/SettingsModal.tsx
+++ b/src/settings/SettingsModal.tsx
@@ -26,7 +26,7 @@ import { ReactComponent as VideoIcon } from "../icons/Video.svg";
 import { ReactComponent as DeveloperIcon } from "../icons/Developer.svg";
 import { ReactComponent as OverflowIcon } from "../icons/Overflow.svg";
 import { SelectInput } from "../input/SelectInput";
-import { MediaDevicesState } from "../room/devices/useMediaDevices";
+import { MediaDevicesState } from "./mediaDevices";
 import {
   useKeyboardShortcuts,
   useSpatialAudio,
@@ -63,7 +63,7 @@ export const SettingsModal = (props: Props) => {
     return (
       <SelectInput
         label={caption}
-        selectedKey={devices.available[devices.selected].deviceId}
+        selectedKey={devices.selectedId}
         onSelectionChange={(id) =>
           props.mediaDevices.selectActiveDevice(kind, id.toString())
         }
diff --git a/src/settings/mediaDevices.ts b/src/settings/mediaDevices.ts
new file mode 100644
index 0000000..5bf33b9
--- /dev/null
+++ b/src/settings/mediaDevices.ts
@@ -0,0 +1,12 @@
+export type MediaDevices = {
+  available: MediaDeviceInfo[];
+  selectedId: string;
+};
+
+export type MediaDevicesState = {
+  state: Map<MediaDeviceKind, MediaDevices>;
+  selectActiveDevice: (
+    kind: MediaDeviceKind,
+    deviceId: string
+  ) => Promise<void>;
+};