add a toggle audio output button

pull/1993/head
Audric Ackermann 3 years ago
parent dafb536f58
commit 8c9832f118
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4

@ -449,6 +449,7 @@
"callMissedTitle": "Call missed",
"noCameraFound": "No camera found",
"noAudioInputFound": "No audio input found",
"noAudioOutputFound": "No audio output found",
"callMediaPermissionsTitle": "Voice and video calls",
"callMissedCausePermission": "Call missed from '$name$' because you need to enable the 'Voice and video calls' permission in the Privacy Settings.",
"callMediaPermissionsDescription": "Allows access to accept voice and video calls from other users",

@ -11,6 +11,7 @@ import styled from 'styled-components';
const videoTriggerId = 'video-menu-trigger-id';
const audioTriggerId = 'audio-menu-trigger-id';
const audioOutputTriggerId = 'audio-output-menu-trigger-id';
export const VideoInputButton = ({
currentConnectedCameras,
@ -68,6 +69,37 @@ export const AudioInputButton = ({
);
};
export const AudioOutputButton = ({
currentConnectedAudioOutputs,
isAudioOutputMuted,
hideArrowIcon = false,
}: {
currentConnectedAudioOutputs: Array<InputItem>;
isAudioOutputMuted: boolean;
hideArrowIcon?: boolean;
}) => {
return (
<>
<DropDownAndToggleButton
iconType="volume"
isMuted={isAudioOutputMuted}
onMainButtonClick={() => {
void handleSpeakerToggle(currentConnectedAudioOutputs, isAudioOutputMuted);
}}
onArrowClick={e => {
showAudioOutputMenu(currentConnectedAudioOutputs, e);
}}
hidePopoverArrow={hideArrowIcon}
/>
<AudioOutputMenu
triggerId={audioOutputTriggerId}
audioOutputsList={currentConnectedAudioOutputs}
/>
</>
);
};
const VideoInputMenu = ({
triggerId,
camerasList,
@ -118,6 +150,31 @@ const AudioInputMenu = ({
);
};
const AudioOutputMenu = ({
triggerId,
audioOutputsList,
}: {
triggerId: string;
audioOutputsList: Array<InputItem>;
}) => {
return (
<Menu id={triggerId} animation={animation.fade}>
{audioOutputsList.map(m => {
return (
<Item
key={m.deviceId}
onClick={() => {
void CallManager.selectAudioOutputByDeviceId(m.deviceId);
}}
>
{m.label.substr(0, 40)}
</Item>
);
})}
</Menu>
);
};
const ShowInFullScreenButton = ({ isFullScreen }: { isFullScreen: boolean }) => {
const dispatch = useDispatch();
@ -181,6 +238,20 @@ const showAudioInputMenu = (
});
};
const showAudioOutputMenu = (
currentConnectedAudioOutputs: Array<any>,
e: React.MouseEvent<HTMLDivElement>
) => {
if (currentConnectedAudioOutputs.length === 0) {
ToastUtils.pushNoAudioOutputFound();
return;
}
contextMenu.show({
id: audioOutputTriggerId,
event: e,
});
};
const showVideoInputMenu = (
currentConnectedCameras: Array<InputItem>,
e: React.MouseEvent<HTMLDivElement>
@ -208,7 +279,7 @@ const handleCameraToggle = async (
// select the first one
await CallManager.selectCameraByDeviceId(currentConnectedCameras[0].deviceId);
} else {
await CallManager.selectCameraByDeviceId(CallManager.INPUT_DISABLED_DEVICE_ID);
await CallManager.selectCameraByDeviceId(CallManager.DEVICE_DISABLED_DEVICE_ID);
}
};
@ -225,7 +296,24 @@ const handleMicrophoneToggle = async (
// selects the first one
await CallManager.selectAudioInputByDeviceId(currentConnectedAudioInputs[0].deviceId);
} else {
await CallManager.selectAudioInputByDeviceId(CallManager.INPUT_DISABLED_DEVICE_ID);
await CallManager.selectAudioInputByDeviceId(CallManager.DEVICE_DISABLED_DEVICE_ID);
}
};
const handleSpeakerToggle = async (
currentConnectedAudioOutputs: Array<InputItem>,
isAudioOutputMuted: boolean
) => {
if (!currentConnectedAudioOutputs.length) {
ToastUtils.pushNoAudioInputFound();
return;
}
if (isAudioOutputMuted) {
// selects the first one
await CallManager.selectAudioOutputByDeviceId(currentConnectedAudioOutputs[0].deviceId);
} else {
await CallManager.selectAudioOutputByDeviceId(CallManager.DEVICE_DISABLED_DEVICE_ID);
}
};
@ -256,15 +344,19 @@ const StyledCallWindowControls = styled.div`
export const CallWindowControls = ({
currentConnectedCameras,
currentConnectedAudioInputs,
currentConnectedAudioOutputs,
isAudioMuted,
isAudioOutputMuted,
remoteStreamVideoIsMuted,
localStreamVideoIsMuted,
isFullScreen,
}: {
isAudioMuted: boolean;
isAudioOutputMuted: boolean;
localStreamVideoIsMuted: boolean;
remoteStreamVideoIsMuted: boolean;
currentConnectedAudioInputs: Array<InputItem>;
currentConnectedAudioOutputs: Array<InputItem>;
currentConnectedCameras: Array<InputItem>;
isFullScreen: boolean;
}) => {
@ -282,6 +374,11 @@ export const CallWindowControls = ({
isAudioMuted={isAudioMuted}
hideArrowIcon={isFullScreen}
/>
<AudioOutputButton
currentConnectedAudioOutputs={currentConnectedAudioOutputs}
isAudioOutputMuted={isAudioOutputMuted}
hideArrowIcon={isFullScreen}
/>
<HangUpButton />
</StyledCallWindowControls>
);

@ -35,8 +35,10 @@ export const CallInFullScreenContainer = () => {
remoteStream,
remoteStreamVideoIsMuted,
currentConnectedAudioInputs,
currentConnectedAudioOutputs,
currentConnectedCameras,
isAudioMuted,
isAudioOutputMuted,
localStreamVideoIsMuted,
} = useVideoCallEventsListener('CallInFullScreenContainer', true);
@ -76,8 +78,10 @@ export const CallInFullScreenContainer = () => {
/>
<CallWindowControls
currentConnectedAudioInputs={currentConnectedAudioInputs}
currentConnectedAudioOutputs={currentConnectedAudioOutputs}
currentConnectedCameras={currentConnectedCameras}
isAudioMuted={isAudioMuted}
isAudioOutputMuted={isAudioOutputMuted}
localStreamVideoIsMuted={localStreamVideoIsMuted}
remoteStreamVideoIsMuted={remoteStreamVideoIsMuted}
isFullScreen={true}

@ -134,11 +134,13 @@ export const InConversationCallContainer = () => {
const {
currentConnectedAudioInputs,
currentConnectedCameras,
currentConnectedAudioOutputs,
localStream,
localStreamVideoIsMuted,
remoteStream,
remoteStreamVideoIsMuted,
isAudioMuted,
isAudioOutputMuted,
} = useVideoCallEventsListener('InConversationCallContainer', true);
if (videoRefRemote?.current && videoRefLocal?.current) {
@ -200,6 +202,8 @@ export const InConversationCallContainer = () => {
currentConnectedAudioInputs={currentConnectedAudioInputs}
currentConnectedCameras={currentConnectedCameras}
isAudioMuted={isAudioMuted}
currentConnectedAudioOutputs={currentConnectedAudioOutputs}
isAudioOutputMuted={isAudioOutputMuted}
localStreamVideoIsMuted={localStreamVideoIsMuted}
remoteStreamVideoIsMuted={remoteStreamVideoIsMuted}
isFullScreen={false}

@ -8,7 +8,7 @@ type SProps = {
onMainButtonClick: (e: React.MouseEvent<HTMLDivElement>) => void;
isMuted?: boolean;
hidePopoverArrow?: boolean;
iconType: 'microphone' | 'camera';
iconType: 'microphone' | 'camera' | 'volume';
};
const StyledRoundedButton = styled.div<{ isMuted: boolean }>`
@ -53,6 +53,12 @@ const CameraIcon = (
</svg>
);
const SpeakerIcon = (
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z" />
</svg>
);
const MicrophoneIcon = (
<svg viewBox="0 0 58 58" fill="currentColor">
<path d="M44,28c-0.552,0-1,0.447-1,1v6c0,7.72-6.28,14-14,14s-14-6.28-14-14v-6c0-0.553-0.448-1-1-1s-1,0.447-1,1v6c0,8.485,6.644,15.429,15,15.949V56h-5c-0.552,0-1,0.447-1,1s0.448,1,1,1h12c0.552,0,1-0.447,1-1s-0.448-1-1-1h-5v-5.051c8.356-0.52,15-7.465,15-15.949v-6C45,28.447,44.552,28,44,28zM29,46c6.065,0,11-4.935,11-11V11c0-6.065-4.935-11-11-11S18,4.935,18,11v24C18,41.065,22.935,46,29,46z" />
@ -72,7 +78,7 @@ export const DropDownAndToggleButton = (props: SProps) => {
onMainButtonClick(e);
};
const iconToRender =
iconType === 'microphone' ? MicrophoneIcon : iconType === 'camera' ? CameraIcon : null;
iconType === 'microphone' ? MicrophoneIcon : iconType === 'camera' ? CameraIcon : SpeakerIcon;
return (
<StyledContainer isMuted={isMuted || false}>

@ -19,6 +19,7 @@ export function useVideoCallEventsListener(uniqueId: string, onSame: boolean) {
const [remoteStream, setRemoteStream] = useState<MediaStream | null>(null);
const [localStreamVideoIsMuted, setLocalStreamVideoIsMuted] = useState(true);
const [ourAudioIsMuted, setOurAudioIsMuted] = useState(false);
const [ourAudioOutputIsMuted, setOurAudioOutputIsMuted] = useState(false);
const [remoteStreamVideoIsMuted, setRemoteStreamVideoIsMuted] = useState(true);
const mountedState = useMountedState();
@ -26,6 +27,11 @@ export function useVideoCallEventsListener(uniqueId: string, onSame: boolean) {
const [currentConnectedAudioInputs, setCurrentConnectedAudioInputs] = useState<Array<InputItem>>(
[]
);
const [currentConnectedAudioOutputs, setCurrentConnectedAudioOutputs] = useState<
Array<InputItem>
>([]);
useEffect(() => {
if (
(onSame && ongoingCallPubkey === selectedConversationKey) ||
@ -34,12 +40,14 @@ export function useVideoCallEventsListener(uniqueId: string, onSame: boolean) {
CallManager.addVideoEventsListener(uniqueId, (options: CallManagerOptionsType) => {
const {
audioInputsList,
audioOutputsList,
camerasList,
isLocalVideoStreamMuted,
isRemoteVideoStreamMuted,
localStream: lLocalStream,
remoteStream: lRemoteStream,
isAudioMuted,
isAudioOutputMuted,
} = options;
if (mountedState()) {
setLocalStream(lLocalStream);
@ -47,9 +55,11 @@ export function useVideoCallEventsListener(uniqueId: string, onSame: boolean) {
setRemoteStreamVideoIsMuted(isRemoteVideoStreamMuted);
setLocalStreamVideoIsMuted(isLocalVideoStreamMuted);
setOurAudioIsMuted(isAudioMuted);
setOurAudioOutputIsMuted(isAudioOutputMuted);
setCurrentConnectedCameras(camerasList);
setCurrentConnectedAudioInputs(audioInputsList);
setCurrentConnectedAudioOutputs(audioOutputsList);
}
});
}
@ -61,11 +71,13 @@ export function useVideoCallEventsListener(uniqueId: string, onSame: boolean) {
return {
currentConnectedAudioInputs,
currentConnectedAudioOutputs,
currentConnectedCameras,
localStreamVideoIsMuted,
remoteStreamVideoIsMuted,
localStream,
remoteStream,
isAudioMuted: ourAudioIsMuted,
isAudioOutputMuted: ourAudioOutputIsMuted,
};
}

@ -37,9 +37,11 @@ export type CallManagerOptionsType = {
remoteStream: MediaStream | null;
camerasList: Array<InputItem>;
audioInputsList: Array<InputItem>;
audioOutputsList: Array<InputItem>;
isLocalVideoStreamMuted: boolean;
isRemoteVideoStreamMuted: boolean;
isAudioMuted: boolean;
isAudioOutputMuted: boolean;
};
export type CallManagerListener = ((options: CallManagerOptionsType) => void) | null;
@ -53,9 +55,11 @@ function callVideoListeners() {
remoteStream,
camerasList,
audioInputsList,
audioOutputsList,
isRemoteVideoStreamMuted: remoteVideoStreamIsMuted,
isLocalVideoStreamMuted: selectedCameraId === INPUT_DISABLED_DEVICE_ID,
isAudioMuted: selectedAudioInputId === INPUT_DISABLED_DEVICE_ID,
isLocalVideoStreamMuted: selectedCameraId === DEVICE_DISABLED_DEVICE_ID,
isAudioMuted: selectedAudioInputId === DEVICE_DISABLED_DEVICE_ID,
isAudioOutputMuted: selectedAudioOutputId === DEVICE_DISABLED_DEVICE_ID,
});
});
}
@ -90,7 +94,7 @@ let remoteStream: MediaStream | null;
let mediaDevices: MediaStream | null;
let remoteVideoStreamIsMuted = true;
export const INPUT_DISABLED_DEVICE_ID = 'off';
export const DEVICE_DISABLED_DEVICE_ID = 'off';
let makingOffer = false;
let ignoreOffer = false;
@ -108,12 +112,14 @@ const configuration: RTCConfiguration = {
// iceTransportPolicy: 'relay', // for now, this cause the connection to break after 30-40 sec if we enable this
};
let selectedCameraId: string = INPUT_DISABLED_DEVICE_ID;
let selectedAudioInputId: string = INPUT_DISABLED_DEVICE_ID;
let selectedCameraId: string = DEVICE_DISABLED_DEVICE_ID;
let selectedAudioInputId: string = DEVICE_DISABLED_DEVICE_ID;
let selectedAudioOutputId: string = DEVICE_DISABLED_DEVICE_ID;
let camerasList: Array<InputItem> = [];
let audioInputsList: Array<InputItem> = [];
let audioOutputsList: Array<InputItem> = [];
async function getConnectedDevices(type: 'videoinput' | 'audioinput') {
async function getConnectedDevices(type: 'videoinput' | 'audioinput' | 'audiooutput') {
const devices = await navigator.mediaDevices.enumerateDevices();
return devices.filter(device => device.kind === type);
}
@ -122,11 +128,12 @@ async function getConnectedDevices(type: 'videoinput' | 'audioinput') {
// tslint:disable-next-line: no-typeof-undefined
if (typeof navigator !== 'undefined') {
navigator.mediaDevices.addEventListener('devicechange', async () => {
await updateInputLists();
await updateConnectedDevices();
callVideoListeners();
});
}
async function updateInputLists() {
async function updateConnectedDevices() {
// Get the set of cameras connected
const videoCameras = await getConnectedDevices('videoinput');
@ -141,10 +148,17 @@ async function updateInputLists() {
deviceId: m.deviceId,
label: m.label,
}));
// Get the set of audio outputs connected
const audiosOutput = await getConnectedDevices('audiooutput');
audioOutputsList = audiosOutput.map(m => ({
deviceId: m.deviceId,
label: m.label,
}));
}
function sendVideoStatusViaDataChannel() {
const videoEnabledLocally = selectedCameraId !== INPUT_DISABLED_DEVICE_ID;
const videoEnabledLocally = selectedCameraId !== DEVICE_DISABLED_DEVICE_ID;
const stringToSend = JSON.stringify({
video: videoEnabledLocally,
});
@ -163,8 +177,8 @@ function sendHangupViaDataChannel() {
}
export async function selectCameraByDeviceId(cameraDeviceId: string) {
if (cameraDeviceId === INPUT_DISABLED_DEVICE_ID) {
selectedCameraId = INPUT_DISABLED_DEVICE_ID;
if (cameraDeviceId === DEVICE_DISABLED_DEVICE_ID) {
selectedCameraId = DEVICE_DISABLED_DEVICE_ID;
const sender = peerConnection?.getSenders().find(s => {
return s.track?.kind === 'video';
@ -214,8 +228,9 @@ export async function selectCameraByDeviceId(cameraDeviceId: string) {
}
}
}
export async function selectAudioInputByDeviceId(audioInputDeviceId: string) {
if (audioInputDeviceId === INPUT_DISABLED_DEVICE_ID) {
if (audioInputDeviceId === DEVICE_DISABLED_DEVICE_ID) {
selectedAudioInputId = audioInputDeviceId;
const sender = peerConnection?.getSenders().find(s => {
@ -260,6 +275,23 @@ export async function selectAudioInputByDeviceId(audioInputDeviceId: string) {
}
}
export async function selectAudioOutputByDeviceId(audioOutputDeviceId: string) {
if (audioOutputDeviceId === DEVICE_DISABLED_DEVICE_ID) {
selectedAudioOutputId = audioOutputDeviceId;
console.warn('selectedAudioOutputId', selectedAudioOutputId);
callVideoListeners();
return;
}
if (audioOutputsList.some(m => m.deviceId === audioOutputDeviceId)) {
selectedAudioOutputId = audioOutputDeviceId;
console.warn('selectedAudioOutputId', selectedAudioOutputId);
callVideoListeners();
}
}
async function handleNegotiationNeededEvent(_event: Event, recipient: string) {
try {
makingOffer = true;
@ -312,7 +344,7 @@ function handleIceCandidates(event: RTCPeerConnectionIceEvent, pubkey: string) {
async function openMediaDevicesAndAddTracks() {
try {
await updateInputLists();
await updateConnectedDevices();
if (!camerasList.length) {
ToastUtils.pushNoCameraFound();
return;
@ -323,7 +355,7 @@ async function openMediaDevicesAndAddTracks() {
}
selectedAudioInputId = audioInputsList[0].deviceId;
selectedCameraId = INPUT_DISABLED_DEVICE_ID;
selectedCameraId = DEVICE_DISABLED_DEVICE_ID;
window.log.info(
`openMediaDevices videoDevice:${selectedCameraId}:${camerasList[0].label} audioDevice:${selectedAudioInputId}`
);
@ -369,7 +401,7 @@ export async function USER_callRecipient(recipient: string) {
);
return;
}
await updateInputLists();
await updateConnectedDevices();
window?.log?.info(`starting call with ${ed25519Str(recipient)}..`);
window.inboxStore?.dispatch(startingCallWith({ pubkey: recipient }));
if (peerConnection) {
@ -462,6 +494,7 @@ function handleConnectionStateChanged(pubkey: string) {
if (peerConnection?.signalingState === 'closed') {
closeVideoCall();
} else if (peerConnection?.connectionState === 'connected') {
setIsRinging(false);
window.inboxStore?.dispatch(callConnected({ pubkey }));
}
}
@ -499,8 +532,8 @@ function closeVideoCall() {
mediaDevices = null;
remoteStream = null;
selectedCameraId = INPUT_DISABLED_DEVICE_ID;
selectedAudioInputId = INPUT_DISABLED_DEVICE_ID;
selectedCameraId = DEVICE_DISABLED_DEVICE_ID;
selectedAudioInputId = DEVICE_DISABLED_DEVICE_ID;
currentCallUUID = undefined;
callVideoListeners();
window.inboxStore?.dispatch(setFullScreenCall(false));
@ -612,7 +645,7 @@ export async function USER_acceptIncomingCallRequest(fromSender: string) {
);
return;
}
await updateInputLists();
await updateConnectedDevices();
const lastOfferMessage = findLastMessageTypeFromSender(
fromSender,
@ -830,18 +863,19 @@ export async function handleCallTypeOffer(
await peerConnection.setRemoteDescription(remoteDesc); // SRD rolls back as needed
await buildAnswerAndSendIt(sender);
}
} else {
window.inboxStore?.dispatch(incomingCall({ pubkey: sender }));
// show a notification
const callerConvo = getConversationController().get(sender);
const convNotif = callerConvo?.get('triggerNotificationsFor') || 'disabled';
if (convNotif === 'disabled') {
window?.log?.info('notifications disabled for convo', ed25519Str(sender));
} else if (callerConvo) {
await callerConvo.notifyIncomingCall();
}
setIsRinging(true);
}
window.inboxStore?.dispatch(incomingCall({ pubkey: sender }));
// show a notification
const callerConvo = getConversationController().get(sender);
const convNotif = callerConvo?.get('triggerNotificationsFor') || 'disabled';
if (convNotif === 'disabled') {
window?.log?.info('notifications disabled for convo', ed25519Str(sender));
} else if (callerConvo) {
await callerConvo.notifyIncomingCall();
}
setIsRinging(true);
pushCallMessageToCallCache(sender, remoteCallUUID, callMessage);
} catch (err) {

@ -266,3 +266,7 @@ export function pushNoCameraFound() {
export function pushNoAudioInputFound() {
pushToastWarning('noAudioInputFound', window.i18n('noAudioInputFound'));
}
export function pushNoAudioOutputFound() {
pushToastWarning('noAudioInputFound', window.i18n('noAudioOutputFound'));
}

Loading…
Cancel
Save