Skip to content

Commit a7fd5b9

Browse files
committed
feat(react): allow using multiple device kinds with MediaDeviceMenu
This is an update to the MediaDeviceMenu react component to allow specifying device kinds as an array (e.g. microphone + speaker). - The menu already rendered a multi-kind menu when no kind was specified, there was just no way to specify >1 kind to render when specifying a kind. This PR leverages that same rendering. - This should be API-compatible. The one type-change was that the existing `MediaDeviceMenuProps` interface which is now a type alias for the prior props interface (now renamed `MediaDeviceMenuPropsSingleKind`) and the new `MediaDeviceMenuPropsMultiKind` props interface. - If you want any changes, feel free to ask, just wanted to see if this was directionally aligned with something you might accept. Signed-off-by: Scott Willeke <[email protected]>
1 parent 8e383d1 commit a7fd5b9

File tree

1 file changed

+59
-31
lines changed

1 file changed

+59
-31
lines changed

packages/react/src/prefabs/MediaDeviceMenu.tsx

Lines changed: 59 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@ import * as React from 'react';
33
import { MediaDeviceSelect } from '../components/controls/MediaDeviceSelect';
44
import type { LocalAudioTrack, LocalVideoTrack } from 'livekit-client';
55

6-
/** @public */
7-
export interface MediaDeviceMenuProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
6+
interface KindWithInitialSelection {
7+
kind: MediaDeviceKind;
8+
initialSelection?: string;
9+
}
10+
11+
interface MediaDeviceMenuPropsSingleKind
12+
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
813
kind?: MediaDeviceKind;
914
initialSelection?: string;
1015
onActiveDeviceChange?: (kind: MediaDeviceKind, deviceId: string) => void;
@@ -21,6 +26,27 @@ export interface MediaDeviceMenuProps extends React.ButtonHTMLAttributes<HTMLBut
2126
requestPermissions?: boolean;
2227
}
2328

29+
interface MediaDeviceMenuPropsMultiKind
30+
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
31+
kind?: KindWithInitialSelection[];
32+
initialSelection?: undefined;
33+
onActiveDeviceChange?: (kind: MediaDeviceKind, deviceId: string) => void;
34+
tracks?: Partial<Record<MediaDeviceKind, LocalAudioTrack | LocalVideoTrack | undefined>>;
35+
/**
36+
* this will call getUserMedia if the permissions are not yet given to enumerate the devices with device labels.
37+
* in some browsers multiple calls to getUserMedia result in multiple permission prompts.
38+
* It's generally advised only flip this to true, once a (preview) track has been acquired successfully with the
39+
* appropriate permissions.
40+
*
41+
* @see {@link PreJoin}
42+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/enumerateDevices | MDN enumerateDevices}
43+
*/
44+
requestPermissions?: boolean;
45+
}
46+
47+
/** @public */
48+
export type MediaDeviceMenuProps = MediaDeviceMenuPropsSingleKind | MediaDeviceMenuPropsMultiKind;
49+
2450
/**
2551
* The `MediaDeviceMenu` component is a button that opens a menu that lists
2652
* all media devices and allows the user to select them.
@@ -101,6 +127,26 @@ export function MediaDeviceMenu({
101127
};
102128
}, [handleClickOutside]);
103129

130+
// Normalize props to a consistent internal format
131+
const kindsWithInitialSelection: KindWithInitialSelection[] = (() => {
132+
if (kind === undefined) {
133+
// Default to audio and video inputs when no kind is specified
134+
return [{ kind: 'audioinput' as MediaDeviceKind }, { kind: 'videoinput' as MediaDeviceKind }];
135+
} else if (Array.isArray(kind)) {
136+
// multi-kind case: kind is KindWithInitialSelection[]
137+
return kind;
138+
} else {
139+
// single kind case: kind is MediaDeviceKind, initialSelection is string | undefined
140+
return [{ kind, initialSelection }];
141+
}
142+
})();
143+
144+
const kindLabels: Record<MediaDeviceKind, string> = {
145+
audioinput: 'Audio inputs',
146+
videoinput: 'Video inputs',
147+
audiooutput: 'Audio outputs',
148+
};
149+
104150
return (
105151
<>
106152
<button
@@ -119,39 +165,21 @@ export function MediaDeviceMenu({
119165
ref={tooltip}
120166
style={{ visibility: isOpen ? 'visible' : 'hidden' }}
121167
>
122-
{kind ? (
123-
<MediaDeviceSelect
124-
initialSelection={initialSelection}
125-
onActiveDeviceChange={(deviceId) => handleActiveDeviceChange(kind, deviceId)}
126-
onDeviceListChange={setDevices}
127-
kind={kind}
128-
track={tracks?.[kind]}
129-
requestPermissions={needPermissions}
130-
/>
131-
) : (
132-
<>
133-
<div className="lk-device-menu-heading">Audio inputs</div>
134-
<MediaDeviceSelect
135-
kind="audioinput"
136-
onActiveDeviceChange={(deviceId) =>
137-
handleActiveDeviceChange('audioinput', deviceId)
138-
}
139-
onDeviceListChange={setDevices}
140-
track={tracks?.audioinput}
141-
requestPermissions={needPermissions}
142-
/>
143-
<div className="lk-device-menu-heading">Video inputs</div>
168+
{kindsWithInitialSelection.map((kindInfo, idx, arr) => (
169+
<React.Fragment key={`device-group-${kindInfo.kind}`}>
170+
{arr.length > 1 && idx < arr.length && (
171+
<div className="lk-device-menu-heading">{kindLabels[kindInfo.kind]}</div>
172+
)}
144173
<MediaDeviceSelect
145-
kind="videoinput"
146-
onActiveDeviceChange={(deviceId) =>
147-
handleActiveDeviceChange('videoinput', deviceId)
148-
}
174+
kind={kindInfo.kind}
175+
initialSelection={kindInfo.initialSelection}
176+
onActiveDeviceChange={(deviceId) => handleActiveDeviceChange(kindInfo.kind, deviceId)}
149177
onDeviceListChange={setDevices}
150-
track={tracks?.videoinput}
178+
track={tracks?.[kindInfo.kind]}
151179
requestPermissions={needPermissions}
152180
/>
153-
</>
154-
)}
181+
</React.Fragment>
182+
))}
155183
</div>
156184
)}
157185
</>

0 commit comments

Comments
 (0)