Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 57 additions & 29 deletions packages/react/src/prefabs/MediaDeviceMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ import * as React from 'react';
import { MediaDeviceSelect } from '../components/controls/MediaDeviceSelect';
import type { LocalAudioTrack, LocalVideoTrack } from 'livekit-client';

/** @public */
export interface MediaDeviceMenuProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
interface KindWithInitialSelection {
kind: MediaDeviceKind;
initialSelection?: string;
}

interface MediaDeviceMenuPropsSingleKind extends React.ButtonHTMLAttributes<HTMLButtonElement> {
kind?: MediaDeviceKind;
initialSelection?: string;
onActiveDeviceChange?: (kind: MediaDeviceKind, deviceId: string) => void;
Expand All @@ -21,6 +25,26 @@ export interface MediaDeviceMenuProps extends React.ButtonHTMLAttributes<HTMLBut
requestPermissions?: boolean;
}

interface MediaDeviceMenuPropsMultiKind extends React.ButtonHTMLAttributes<HTMLButtonElement> {
kind?: KindWithInitialSelection[];
initialSelection?: undefined;
onActiveDeviceChange?: (kind: MediaDeviceKind, deviceId: string) => void;
tracks?: Partial<Record<MediaDeviceKind, LocalAudioTrack | LocalVideoTrack | undefined>>;
/**
* this will call getUserMedia if the permissions are not yet given to enumerate the devices with device labels.
* in some browsers multiple calls to getUserMedia result in multiple permission prompts.
* It's generally advised only flip this to true, once a (preview) track has been acquired successfully with the
* appropriate permissions.
*
* @see {@link PreJoin}
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/enumerateDevices | MDN enumerateDevices}
*/
requestPermissions?: boolean;
}

/** @public */
export type MediaDeviceMenuProps = MediaDeviceMenuPropsSingleKind | MediaDeviceMenuPropsMultiKind;

/**
* The `MediaDeviceMenu` component is a button that opens a menu that lists
* all media devices and allows the user to select them.
Expand Down Expand Up @@ -101,6 +125,26 @@ export function MediaDeviceMenu({
};
}, [handleClickOutside]);

// Normalize props to a consistent internal format
const kindsWithInitialSelection: KindWithInitialSelection[] = (() => {
if (kind === undefined) {
// Default to audio and video inputs when no kind is specified
return [{ kind: 'audioinput' as MediaDeviceKind }, { kind: 'videoinput' as MediaDeviceKind }];
} else if (Array.isArray(kind)) {
// multi-kind case: kind is KindWithInitialSelection[]
return kind;
} else {
// single kind case: kind is MediaDeviceKind, initialSelection is string | undefined
return [{ kind, initialSelection }];
}
})();

const kindLabels: Record<MediaDeviceKind, string> = {
audioinput: 'Audio inputs',
videoinput: 'Video inputs',
audiooutput: 'Audio outputs',
};

return (
<>
<button
Expand All @@ -119,39 +163,23 @@ export function MediaDeviceMenu({
ref={tooltip}
style={{ visibility: isOpen ? 'visible' : 'hidden' }}
>
{kind ? (
<MediaDeviceSelect
initialSelection={initialSelection}
onActiveDeviceChange={(deviceId) => handleActiveDeviceChange(kind, deviceId)}
onDeviceListChange={setDevices}
kind={kind}
track={tracks?.[kind]}
requestPermissions={needPermissions}
/>
) : (
<>
<div className="lk-device-menu-heading">Audio inputs</div>
<MediaDeviceSelect
kind="audioinput"
onActiveDeviceChange={(deviceId) =>
handleActiveDeviceChange('audioinput', deviceId)
}
onDeviceListChange={setDevices}
track={tracks?.audioinput}
requestPermissions={needPermissions}
/>
<div className="lk-device-menu-heading">Video inputs</div>
{kindsWithInitialSelection.map((kindInfo, idx, arr) => (
<React.Fragment key={`device-group-${kindInfo.kind}`}>
{arr.length > 1 && idx < arr.length && (
<div className="lk-device-menu-heading">{kindLabels[kindInfo.kind]}</div>
)}
<MediaDeviceSelect
kind="videoinput"
kind={kindInfo.kind}
initialSelection={kindInfo.initialSelection}
onActiveDeviceChange={(deviceId) =>
handleActiveDeviceChange('videoinput', deviceId)
handleActiveDeviceChange(kindInfo.kind, deviceId)
}
onDeviceListChange={setDevices}
track={tracks?.videoinput}
track={tracks?.[kindInfo.kind]}
requestPermissions={needPermissions}
/>
</>
)}
</React.Fragment>
))}
</div>
)}
</>
Expand Down
Loading