import React, { useCallback, useEffect, useState } from 'react'; import FocusTrap from 'focus-trap-react'; import { Box, Icon, IconButton, Icons, Menu, MenuItem, PopOut, RectCords, Text, Tooltip, TooltipProvider, config, toRem, } from 'folds'; import { CallEmbed } from '../../plugins/call'; import { MobileTouchTarget } from '../../styles/mobile.css'; import { stopPropagation } from '../../utils/keyboard'; /** Output selection needs setSinkId; Firefox/Safari/Android Chrome lack it. */ export const audioOutputSelectable = (): boolean => typeof HTMLMediaElement !== 'undefined' && 'setSinkId' in HTMLMediaElement.prototype && typeof navigator !== 'undefined' && !!navigator.mediaDevices?.enumerateDevices; type OutputDevice = { id: string; label: string }; /** * [Gitea #119] Speaker button in the call bar: a small menu of audio outputs * (headset ↔ speakers) without opening Settings. The choice goes to the fork * as io.lotus.set_audio_output; the fork's own picker is unreachable here * because the embed hides Element Call's footer. */ export function AudioOutputButton({ embed, disabled }: { embed: CallEmbed; disabled?: boolean }) { const [anchor, setAnchor] = useState(); const [devices, setDevices] = useState([]); const [selected, setSelected] = useState(embed.control.audioOutputId); const refresh = useCallback(async () => { try { const all = await navigator.mediaDevices.enumerateDevices(); setDevices( all .filter((d) => d.kind === 'audiooutput') .map((d, i) => ({ id: d.deviceId, label: d.label || `Output ${i + 1}` })), ); } catch { setDevices([]); } }, []); useEffect(() => { if (!anchor) return undefined; refresh(); navigator.mediaDevices.addEventListener('devicechange', refresh); return () => navigator.mediaDevices.removeEventListener('devicechange', refresh); }, [anchor, refresh]); const choose = (id: string) => { embed.control.setAudioOutput(id); setSelected(id); setAnchor(undefined); }; return ( setAnchor(undefined), clickOutsideDeactivates: true, escapeDeactivates: stopPropagation, }} > {devices.length === 0 && ( No audio outputs found. )} {devices.map((d) => { const isSelected = selected ? d.id === selected : d.id === 'default'; return ( : undefined} onClick={() => choose(d.id)} > {d.label} ); })} } > Audio output } > {(tipRef) => ( setAnchor(e.currentTarget.getBoundingClientRect())} > )} ); }