From 6a1f575c46c4cddd74816505325e88e375fc4a35 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 28 Oct 2021 16:10:28 +1100 Subject: [PATCH] create a hook for listening for video call events + wip fullscreen video calls --- ts/components/session/ActionsPanel.tsx | 15 +- .../session/calling/CallContainer.tsx | 33 +- .../calling/CallInFullScreenContainer.tsx | 47 +++ .../calling/InConversationCallContainer.tsx | 301 +++++++++--------- ts/components/session/icon/Icons.tsx | 7 + ts/hooks/useVideoEventListener.ts | 68 ++++ ts/session/utils/CallManager.ts | 101 +++--- ts/state/ducks/conversations.ts | 9 + ts/state/selectors/conversations.ts | 10 + 9 files changed, 370 insertions(+), 221 deletions(-) create mode 100644 ts/components/session/calling/CallInFullScreenContainer.tsx create mode 100644 ts/hooks/useVideoEventListener.ts diff --git a/ts/components/session/ActionsPanel.tsx b/ts/components/session/ActionsPanel.tsx index ef9a246d9..612d75909 100644 --- a/ts/components/session/ActionsPanel.tsx +++ b/ts/components/session/ActionsPanel.tsx @@ -48,6 +48,7 @@ import { ActionPanelOnionStatusLight } from '../dialog/OnionStatusPathDialog'; import { switchHtmlToDarkTheme, switchHtmlToLightTheme } from '../../state/ducks/SessionTheme'; import { DraggableCallContainer } from './calling/CallContainer'; import { IncomingCallDialog } from './calling/IncomingCallDialog'; +import { CallInFullScreenContainer } from './calling/CallInFullScreenContainer'; const Section = (props: { type: SectionType; avatarPath?: string | null }) => { const ourNumber = useSelector(getOurNumber); @@ -232,6 +233,16 @@ const doAppStartUp = () => { void getSwarmPollingInstance().start(); }; +const CallContainer = () => { + return ( + <> + + + + + ); +}; + /** * ActionsPanel is the far left banner (not the left pane). * The panel with buttons to switch between the message/contact/settings/theme views @@ -290,9 +301,7 @@ export const ActionsPanel = () => { <> - - - +
diff --git a/ts/components/session/calling/CallContainer.tsx b/ts/components/session/calling/CallContainer.tsx index f31ef3a4b..8f7fabee3 100644 --- a/ts/components/session/calling/CallContainer.tsx +++ b/ts/components/session/calling/CallContainer.tsx @@ -2,11 +2,8 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useSelector } from 'react-redux'; import Draggable, { DraggableData, DraggableEvent } from 'react-draggable'; -// tslint:disable-next-line: no-submodule-imports -import useMountedState from 'react-use/lib/useMountedState'; import styled from 'styled-components'; import _ from 'underscore'; -import { CallManager } from '../../../session/utils'; import { getHasOngoingCall, getHasOngoingCallWith, @@ -15,7 +12,7 @@ import { import { openConversationWithMessages } from '../../../state/ducks/conversations'; import { Avatar, AvatarSize } from '../../Avatar'; import { getConversationController } from '../../../session/conversations'; -import { CallManagerOptionsType } from '../../../session/utils/CallManager'; +import { useVideoCallEventsListener } from '../../../hooks/useVideoEventListener'; export const DraggableCallWindow = styled.div` position: absolute; @@ -74,11 +71,12 @@ export const DraggableCallContainer = () => { const [positionY, setPositionY] = useState(window.innerHeight / 2); const [lastPositionX, setLastPositionX] = useState(0); const [lastPositionY, setLastPositionY] = useState(0); - const [isRemoteVideoMuted, setIsRemoteVideoMuted] = useState(true); const ongoingCallPubkey = ongoingCallProps?.id; + const { remoteStreamVideoIsMuted, remoteStream } = useVideoCallEventsListener( + 'DraggableCallContainer' + ); const videoRefRemote = useRef(undefined); - const mountedState = useMountedState(); function onWindowResize() { if (positionY + 50 > window.innerHeight || positionX + 50 > window.innerWidth) { @@ -95,22 +93,9 @@ export const DraggableCallContainer = () => { }; }, [positionX, positionY]); - useEffect(() => { - if (ongoingCallPubkey !== selectedConversationKey) { - CallManager.setVideoEventsListener( - ({ isRemoteVideoStreamMuted, remoteStream }: CallManagerOptionsType) => { - if (mountedState() && videoRefRemote?.current) { - videoRefRemote.current.srcObject = remoteStream; - setIsRemoteVideoMuted(isRemoteVideoStreamMuted); - } - } - ); - } - - return () => { - CallManager.setVideoEventsListener(null); - }; - }, [ongoingCallPubkey, selectedConversationKey]); + if (videoRefRemote?.current?.srcObject && remoteStream) { + videoRefRemote.current.srcObject = remoteStream; + } const openCallingConversation = useCallback(() => { if (ongoingCallPubkey && ongoingCallPubkey !== selectedConversationKey) { @@ -152,9 +137,9 @@ export const DraggableCallContainer = () => { - {isRemoteVideoMuted && ( + {remoteStreamVideoIsMuted && ( { + const dispatch = useDispatch(); + const ongoingCallProps = useSelector(getHasOngoingCallWith); + // const selectedConversationKey = useSelector(getSelectedConversationKey); + const hasOngoingCall = useSelector(getHasOngoingCall); + const hasOngoingCallFullScreen = useSelector(getCallIsInFullScreen); + + // const ongoingCallPubkey = ongoingCallProps?.id; + // const ongoingCallUsername = ongoingCallProps?.profileName || ongoingCallProps?.name; + // const videoRefRemote = useRef(); + // const videoRefLocal = useRef(); + // const mountedState = useMountedState(); + + function toggleFullScreenOFF() { + dispatch(setFullScreenCall(false)); + } + + if (!hasOngoingCall || !ongoingCallProps || !hasOngoingCallFullScreen) { + return null; + } + + return ; +}; diff --git a/ts/components/session/calling/InConversationCallContainer.tsx b/ts/components/session/calling/InConversationCallContainer.tsx index 7c6a41914..6e7d34371 100644 --- a/ts/components/session/calling/InConversationCallContainer.tsx +++ b/ts/components/session/calling/InConversationCallContainer.tsx @@ -1,23 +1,24 @@ -import React, { useEffect, useRef, useState } from 'react'; -import { useSelector } from 'react-redux'; +import React, { useRef } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; -// tslint:disable-next-line: no-submodule-imports -import useMountedState from 'react-use/lib/useMountedState'; import styled from 'styled-components'; import _ from 'underscore'; import { CallManager, ToastUtils, UserUtils } from '../../../session/utils'; import { getHasOngoingCall, getHasOngoingCallWith, + getHasOngoingCallWithPubkey, getSelectedConversationKey, } from '../../../state/selectors/conversations'; import { SessionIconButton } from '../icon'; import { animation, contextMenu, Item, Menu } from 'react-contexify'; -import { CallManagerOptionsType, InputItem } from '../../../session/utils/CallManager'; +import { InputItem } from '../../../session/utils/CallManager'; import { DropDownAndToggleButton } from '../icon/DropDownAndToggleButton'; import { StyledVideoElement } from './CallContainer'; import { Avatar, AvatarSize } from '../../Avatar'; import { getConversationController } from '../../../session/conversations'; +import { setFullScreenCall } from '../../../state/ducks/conversations'; +import { useVideoCallEventsListener } from '../../../hooks/useVideoEventListener'; const VideoContainer = styled.div` height: 100%; @@ -70,10 +71,8 @@ const RelativeCallWindow = styled.div` const VideoInputMenu = ({ triggerId, camerasList, - onUnmute, }: { triggerId: string; - onUnmute: () => void; camerasList: Array; }) => { return ( @@ -83,7 +82,6 @@ const VideoInputMenu = ({ { - onUnmute(); void CallManager.selectCameraByDeviceId(m.deviceId); }} > @@ -98,11 +96,9 @@ const VideoInputMenu = ({ const AudioInputMenu = ({ triggerId, audioInputsList, - onUnmute, }: { triggerId: string; audioInputsList: Array; - onUnmute: () => void; }) => { return ( @@ -111,7 +107,6 @@ const AudioInputMenu = ({ { - onUnmute(); void CallManager.selectAudioInputByDeviceId(m.deviceId); }} > @@ -136,29 +131,129 @@ const CenteredAvatarInConversation = styled.div` align-items: center; `; +const videoTriggerId = 'video-menu-trigger-id'; +const audioTriggerId = 'audio-menu-trigger-id'; + +const ShowInFullScreenButton = () => { + const dispatch = useDispatch(); + + const showInFullScreen = () => { + dispatch(setFullScreenCall(true)); + }; + + return ( + + ); +}; + +const HangUpButton = () => { + const ongoingCallPubkey = useSelector(getHasOngoingCallWithPubkey); + + const handleEndCall = async () => { + // call method to end call connection + if (ongoingCallPubkey) { + await CallManager.USER_rejectIncomingCallRequest(ongoingCallPubkey); + } + }; + + return ( + + ); +}; + +const showAudioInputMenu = ( + currentConnectedAudioInputs: Array, + e: React.MouseEvent +) => { + if (currentConnectedAudioInputs.length === 0) { + ToastUtils.pushNoAudioInputFound(); + return; + } + contextMenu.show({ + id: audioTriggerId, + event: e, + }); +}; + +const showVideoInputMenu = ( + currentConnectedCameras: Array, + e: React.MouseEvent +) => { + if (currentConnectedCameras.length === 0) { + ToastUtils.pushNoCameraFound(); + return; + } + contextMenu.show({ + id: videoTriggerId, + event: e, + }); +}; + +const handleCameraToggle = async ( + currentConnectedCameras: Array, + localStreamVideoIsMuted: boolean +) => { + if (!currentConnectedCameras.length) { + ToastUtils.pushNoCameraFound(); + + return; + } + if (localStreamVideoIsMuted) { + // select the first one + await CallManager.selectCameraByDeviceId(currentConnectedCameras[0].deviceId); + } else { + await CallManager.selectCameraByDeviceId(CallManager.INPUT_DISABLED_DEVICE_ID); + } +}; + +const handleMicrophoneToggle = async ( + currentConnectedAudioInputs: Array, + isAudioMuted: boolean +) => { + if (!currentConnectedAudioInputs.length) { + ToastUtils.pushNoAudioInputFound(); + + return; + } + console.warn('onclick', isAudioMuted); + if (isAudioMuted) { + // selects the first one + await CallManager.selectAudioInputByDeviceId(currentConnectedAudioInputs[0].deviceId); + } else { + console.warn('onclick was not muted so muting it now'); + + await CallManager.selectAudioInputByDeviceId(CallManager.INPUT_DISABLED_DEVICE_ID); + } +}; + // tslint:disable-next-line: max-func-body-length export const InConversationCallContainer = () => { const ongoingCallProps = useSelector(getHasOngoingCallWith); const selectedConversationKey = useSelector(getSelectedConversationKey); const hasOngoingCall = useSelector(getHasOngoingCall); - const [currentConnectedCameras, setCurrentConnectedCameras] = useState>([]); - const [currentConnectedAudioInputs, setCurrentConnectedAudioInputs] = useState>( - [] - ); - const ongoingCallPubkey = ongoingCallProps?.id; + const ongoingCallPubkey = useSelector(getHasOngoingCallWithPubkey); const ongoingCallUsername = ongoingCallProps?.profileName || ongoingCallProps?.name; const videoRefRemote = useRef(); const videoRefLocal = useRef(); - const mountedState = useMountedState(); - - const [isLocalVideoMuted, setLocalVideoMuted] = useState(true); - const [isRemoteVideoMuted, setIsRemoteVideoMuted] = useState(true); - - const [isAudioMuted, setAudioMuted] = useState(false); - - const videoTriggerId = 'video-menu-trigger-id'; - const audioTriggerId = 'audio-menu-trigger-id'; const remoteAvatarPath = ongoingCallPubkey ? getConversationController() @@ -175,94 +270,20 @@ export const InConversationCallContainer = () => { .get(ourPubkey) .getAvatarPath(); - useEffect(() => { - if (ongoingCallPubkey === selectedConversationKey) { - CallManager.setVideoEventsListener((options: CallManagerOptionsType) => { - const { - audioInputsList, - camerasList, - isLocalVideoStreamMuted, - isRemoteVideoStreamMuted, - localStream, - remoteStream, - } = options; - if (mountedState() && videoRefRemote?.current && videoRefLocal?.current) { - videoRefLocal.current.srcObject = localStream; - setIsRemoteVideoMuted(isRemoteVideoStreamMuted); - setLocalVideoMuted(isLocalVideoStreamMuted); - videoRefRemote.current.srcObject = remoteStream; - - setCurrentConnectedCameras(camerasList); - setCurrentConnectedAudioInputs(audioInputsList); - } - }); - } - - return () => { - CallManager.setVideoEventsListener(null); - }; - }, [ongoingCallPubkey, selectedConversationKey]); - - const handleEndCall = async () => { - // call method to end call connection - if (ongoingCallPubkey) { - await CallManager.USER_rejectIncomingCallRequest(ongoingCallPubkey); - } - }; - - const handleCameraToggle = async () => { - if (!currentConnectedCameras.length) { - ToastUtils.pushNoCameraFound(); - - return; - } - if (isLocalVideoMuted) { - // select the first one - await CallManager.selectCameraByDeviceId(currentConnectedCameras[0].deviceId); - } else { - await CallManager.selectCameraByDeviceId(CallManager.INPUT_DISABLED_DEVICE_ID); - } - - setLocalVideoMuted(!isLocalVideoMuted); - }; - - const handleMicrophoneToggle = async () => { - if (!currentConnectedAudioInputs.length) { - ToastUtils.pushNoAudioInputFound(); - - return; - } - if (isAudioMuted) { - // select the first one - await CallManager.selectAudioInputByDeviceId(currentConnectedAudioInputs[0].deviceId); - } else { - await CallManager.selectAudioInputByDeviceId(CallManager.INPUT_DISABLED_DEVICE_ID); - } - - setAudioMuted(!isAudioMuted); - }; - - const showAudioInputMenu = (e: React.MouseEvent) => { - if (currentConnectedAudioInputs.length === 0) { - ToastUtils.pushNoAudioInputFound(); - return; - } - contextMenu.show({ - id: audioTriggerId, - event: e, - }); - }; - - const showVideoInputMenu = (e: React.MouseEvent) => { - if (currentConnectedCameras.length === 0) { - ToastUtils.pushNoCameraFound(); - return; - } - contextMenu.show({ - id: videoTriggerId, - event: e, - }); - }; + const { + currentConnectedAudioInputs, + currentConnectedCameras, + localStream, + localStreamVideoIsMuted, + remoteStream, + remoteStreamVideoIsMuted, + isAudioMuted, + } = useVideoCallEventsListener('InConversationCallContainer'); + + if (videoRefRemote?.current && videoRefLocal?.current) { + videoRefRemote.current.srcObject = remoteStream; + videoRefLocal.current.srcObject = localStream; + } if (!hasOngoingCall || !ongoingCallProps || ongoingCallPubkey !== selectedConversationKey) { return null; @@ -275,9 +296,9 @@ export const InConversationCallContainer = () => { - {isRemoteVideoMuted && ( + {remoteStreamVideoIsMuted && ( { ref={videoRefLocal} autoPlay={true} muted={true} - isVideoMuted={isLocalVideoMuted} + isVideoMuted={localStreamVideoIsMuted} /> - {isLocalVideoMuted && ( + {localStreamVideoIsMuted && ( { - + { + void handleCameraToggle(currentConnectedCameras, localStreamVideoIsMuted); + }} + onArrowClick={e => { + showVideoInputMenu(currentConnectedCameras, e); + }} /> { + void handleMicrophoneToggle(currentConnectedAudioInputs, isAudioMuted); + }} + onArrowClick={e => { + showAudioInputMenu(currentConnectedAudioInputs, e); + }} /> + - { - setLocalVideoMuted(false); - }} - camerasList={currentConnectedCameras} - /> - { - setAudioMuted(false); - }} - audioInputsList={currentConnectedAudioInputs} - /> + + ); diff --git a/ts/components/session/icon/Icons.tsx b/ts/components/session/icon/Icons.tsx index 8ed5c0b01..77e3ba1af 100644 --- a/ts/components/session/icon/Icons.tsx +++ b/ts/components/session/icon/Icons.tsx @@ -20,6 +20,7 @@ export type SessionIconType = | 'eye' | 'exit' | 'file' + | 'fullscreen' | 'gear' | 'globe' | 'hangup' @@ -196,6 +197,12 @@ export const icons = { viewBox: '0 0 24 24', ratio: 1, }, + fullscreen: { + path: + 'M13,1 C13.0425909,1 13.0845598,1.00266262 13.1257495,1.00783047 L13,1 C13.0528361,1 13.1052411,1.00418141 13.1567725,1.01236099 C13.1883933,1.0172036 13.2193064,1.02361582 13.249662,1.03141743 C13.2598053,1.0342797 13.2698902,1.03704988 13.2799252,1.0399762 C13.3109399,1.04873224 13.3413507,1.05922617 13.3710585,1.07110396 C13.3800191,1.07496957 13.3890567,1.0787342 13.3980377,1.08263089 C13.4262995,1.09463815 13.4536613,1.10806791 13.4802859,1.12267436 C13.4906553,1.12855823 13.5012587,1.13461331 13.5117542,1.14086468 C13.5399066,1.15749759 13.5670269,1.17554946 13.5931738,1.19484452 C13.5995817,1.19963491 13.6064603,1.20483437 13.6132762,1.21012666 C13.6177282,1.21353888 13.6216003,1.21659988 13.625449,1.21968877 L13.7071068,1.29289322 L13.7071068,1.29289322 L20.7071068,8.29289322 C20.7364445,8.32223095 20.7639678,8.3533831 20.7894939,8.38616693 L20.7071068,8.29289322 C20.7429509,8.32873733 20.7757929,8.36702236 20.8054709,8.40735764 C20.8244505,8.43297305 20.8425024,8.46009338 20.8592238,8.48809993 C20.8653867,8.49874131 20.8714418,8.50934473 20.8772982,8.52005033 C20.8919321,8.54633874 20.9053618,8.57370048 20.9175449,8.60172936 C20.9212658,8.61094326 20.9250304,8.61998091 20.9286618,8.62907226 C20.9407738,8.65864932 20.9512678,8.68906007 20.9602981,8.72009403 C20.9629501,8.73010978 20.9657203,8.7401947 20.9683328,8.75032594 C20.9763842,8.78069364 20.9827964,8.81160666 20.9877474,8.84300527 C20.9892866,8.85360724 20.990772,8.86402246 20.9920936,8.8744695 C20.9973374,8.91544017 21,8.95740914 21,9 L21,9 L21,20 C21,21.6568542 19.6568542,23 18,23 L6,23 C4.34314575,23 3,21.6568542 3,20 L3,4 C3,2.34314575 4.34314575,1 6,1 Z M12,3 L6,3 C5.44771525,3 5,3.44771525 5,4 L5,20 C5,20.5522847 5.44771525,21 6,21 L18,21 C18.5522847,21 19,20.5522847 19,20 L19,10 L13,10 C12.4871642,10 12.0644928,9.61395981 12.0067277,9.11662113 L12,9 L12,3 Z M17.586,8 L14,4.415 L14,8 L17.586,8', + viewBox: '0 0 24 24', + ratio: 1, + }, gear: { path: 'M12,0 C13.6568542,0 15,1.34314575 15,3 L15,3.08601169 C15.0010253,3.34508314 15.1558067,3.57880297 15.4037653,3.68513742 C15.6468614,3.79242541 15.9307827,3.74094519 16.1128932,3.56289322 L16.1725,3.50328666 C16.7352048,2.93995553 17.4987723,2.62342669 18.295,2.62342669 C19.0912277,2.62342669 19.8547952,2.93995553 20.4167133,3.5025 C20.9800445,4.06520477 21.2965733,4.82877226 21.2965733,5.625 C21.2965733,6.42122774 20.9800445,7.18479523 20.4171068,7.74710678 L20.3648626,7.79926496 C20.1790548,7.98921731 20.1275746,8.27313857 20.2348626,8.51623466 C20.26314,8.58030647 20.2845309,8.64699387 20.2987985,8.71517468 C20.4176633,8.89040605 20.6163373,8.99914118 20.83,9 L21,9 C22.6568542,9 24,10.3431458 24,12 C24,13.6568542 22.6568542,15 21,15 L20.9139883,15 C20.6549169,15.0010253 20.421197,15.1558067 20.3191398,15.3939314 C20.2075746,15.6468614 20.2590548,15.9307827 20.4371068,16.1128932 L20.4967133,16.1725 C21.0600445,16.7352048 21.3765733,17.4987723 21.3765733,18.295 C21.3765733,19.0912277 21.0600445,19.8547952 20.4975,20.4167133 C19.9347952,20.9800445 19.1712277,21.2965733 18.375,21.2965733 C17.5787723,21.2965733 16.8152048,20.9800445 16.2528932,20.4171068 L16.200735,20.3648626 C16.0107827,20.1790548 15.7268614,20.1275746 15.4739314,20.2391398 C15.2358067,20.341197 15.0810253,20.5749169 15.08,20.83 L15.08,21 C15.08,22.6568542 13.7368542,24 12.08,24 C10.4231458,24 9.08,22.6568542 9.08,21 C9.07403212,20.6665579 8.90531385,20.4306648 8.59623466,20.3148626 C8.35313857,20.2075746 8.06921731,20.2590548 7.88710678,20.4371068 L7.8275,20.4967133 C7.26479523,21.0600445 6.50122774,21.3765733 5.705,21.3765733 C4.90877226,21.3765733 4.14520477,21.0600445 3.58328666,20.4975 C3.01995553,19.9347952 2.70342669,19.1712277 2.70342669,18.375 C2.70342669,17.5787723 3.01995553,16.8152048 3.58289322,16.2528932 L3.63513742,16.200735 C3.82094519,16.0107827 3.87242541,15.7268614 3.76086017,15.4739314 C3.65880297,15.2358067 3.42508314,15.0810253 3.17,15.08 L3,15.08 C1.34314575,15.08 0,13.7368542 0,12.08 C0,10.4231458 1.34314575,9.08 3,9.08 C3.33344206,9.07403212 3.56933519,8.90531385 3.68513742,8.59623466 C3.79242541,8.35313857 3.74094519,8.06921731 3.56289322,7.88710678 L3.50328666,7.8275 C2.93995553,7.26479523 2.62342669,6.50122774 2.62342669,5.705 C2.62342669,4.90877226 2.93995553,4.14520477 3.5025,3.58328666 C4.06520477,3.01995553 4.82877226,2.70342669 5.625,2.70342669 C6.42122774,2.70342669 7.18479523,3.01995553 7.74710678,3.58289322 L7.79926496,3.63513742 C7.98921731,3.82094519 8.27313857,3.87242541 8.51623466,3.76513742 C8.58030647,3.73685997 8.64699387,3.71546911 8.71517468,3.70120146 C8.89040605,3.58233675 8.99914118,3.3836627 9,3.17 L9,3 C9,1.34314575 10.3431458,0 12,0 Z M12,2 C11.4477153,2 11,2.44771525 11,3 L11,3.17398831 C10.9957795,4.2302027 10.3647479,5.18306046 9.39393144,5.59913983 C9.30943133,5.63535548 9.22053528,5.65966354 9.12978593,5.67154209 C8.1847178,6.00283804 7.12462982,5.77295717 6.39289322,5.05710678 L6.3325,4.99671334 C6.14493174,4.8089363 5.89040925,4.70342669 5.625,4.70342669 C5.35959075,4.70342669 5.10506826,4.8089363 4.91671334,4.9975 C4.7289363,5.18506826 4.62342669,5.43959075 4.62342669,5.705 C4.62342669,5.97040925 4.7289363,6.22493174 4.91710678,6.41289322 L4.98486258,6.48073504 C5.74238657,7.25515616 5.9522675,8.41268129 5.5385361,9.34518109 C5.16293446,10.3664297 4.2012163,11.0542811 3.09,11.08 L3,11.08 C2.44771525,11.08 2,11.5277153 2,12.08 C2,12.6322847 2.44771525,13.08 3,13.08 L3.17398831,13.080008 C4.2302027,13.0842205 5.18306046,13.7152521 5.59486258,14.6762347 C6.0322675,15.6673187 5.82238657,16.8248438 5.05710678,17.6071068 L4.99671334,17.6675 C4.8089363,17.8550683 4.70342669,18.1095908 4.70342669,18.375 C4.70342669,18.6404092 4.8089363,18.8949317 4.9975,19.0832867 C5.18506826,19.2710637 5.43959075,19.3765733 5.705,19.3765733 C5.97040925,19.3765733 6.22493174,19.2710637 6.41289322,19.0828932 L6.48073504,19.0151374 C7.25515616,18.2576134 8.41268129,18.0477325 9.34518109,18.4614639 C10.3664297,18.8370655 11.0542811,19.7987837 11.08,20.91 L11.08,21 C11.08,21.5522847 11.5277153,22 12.08,22 C12.6322847,22 13.08,21.5522847 13.08,21 L13.080008,20.8260117 C13.0842205,19.7697973 13.7152521,18.8169395 14.6762347,18.4051374 C15.6673187,17.9677325 16.8248438,18.1776134 17.6071068,18.9428932 L17.6675,19.0032867 C17.8550683,19.1910637 18.1095908,19.2965733 18.375,19.2965733 C18.6404092,19.2965733 18.8949317,19.1910637 19.0832867,19.0025 C19.2710637,18.8149317 19.3765733,18.5604092 19.3765733,18.295 C19.3765733,18.0295908 19.2710637,17.7750683 19.0828932,17.5871068 L19.0151374,17.519265 C18.2576134,16.7448438 18.0477325,15.5873187 18.4851374,14.5962347 C18.8969395,13.6352521 19.8497973,13.0042205 20.91,13 L21,13 C21.5522847,13 22,12.5522847 22,12 C22,11.4477153 21.5522847,11 21,11 L20.8260117,11 C19.7697973,10.9957795 18.8169395,10.3647479 18.4008602,9.39393144 C18.3646445,9.30943133 18.3403365,9.22053528 18.3284579,9.12978593 C17.997162,8.1847178 18.2270428,7.12462982 18.9428932,6.39289322 L19.0032867,6.3325 C19.1910637,6.14493174 19.2965733,5.89040925 19.2965733,5.625 C19.2965733,5.35959075 19.1910637,5.10506826 19.0025,4.91671334 C18.8149317,4.7289363 18.5604092,4.62342669 18.295,4.62342669 C18.0295908,4.62342669 17.7750683,4.7289363 17.5871068,4.91710678 L17.519265,4.98486258 C16.7448438,5.74238657 15.5873187,5.9522675 14.6060686,5.51913983 C13.6352521,5.10306046 13.0042205,4.1502027 13,3.09 L13,3 C13,2.44771525 12.5522847,2 12,2 Z M12,8 C14.209139,8 16,9.790861 16,12 C16,14.209139 14.209139,16 12,16 C9.790861,16 8,14.209139 8,12 C8,9.790861 9.790861,8 12,8 Z M12,14 C13.1045695,14 14,13.1045695 14,12 C14,10.8954305 13.1045695,10 12,10 C10.8954305,10 10,10.8954305 10,12 C10,13.1045695 10.8954305,14 12,14', diff --git a/ts/hooks/useVideoEventListener.ts b/ts/hooks/useVideoEventListener.ts new file mode 100644 index 000000000..61a1476e8 --- /dev/null +++ b/ts/hooks/useVideoEventListener.ts @@ -0,0 +1,68 @@ +import { useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; +// tslint:disable-next-line: no-submodule-imports +import useMountedState from 'react-use/lib/useMountedState'; +import { CallManager } from '../session/utils'; +import { CallManagerOptionsType, InputItem } from '../session/utils/CallManager'; +import { + getCallIsInFullScreen, + getHasOngoingCallWithPubkey, + getSelectedConversationKey, +} from '../state/selectors/conversations'; + +export function useVideoCallEventsListener(uniqueId: string) { + const selectedConversationKey = useSelector(getSelectedConversationKey); + const ongoingCallPubkey = useSelector(getHasOngoingCallWithPubkey); + const isFullScreen = useSelector(getCallIsInFullScreen); + + const [localStream, setLocalStream] = useState(null); + const [remoteStream, setRemoteStream] = useState(null); + const [localStreamVideoIsMuted, setLocalStreamVideoIsMuted] = useState(true); + const [ourAudioIsMuted, setOurAudioIsMuted] = useState(false); + const [remoteStreamVideoIsMuted, setRemoteStreamVideoIsMuted] = useState(true); + const mountedState = useMountedState(); + + const [currentConnectedCameras, setCurrentConnectedCameras] = useState>([]); + const [currentConnectedAudioInputs, setCurrentConnectedAudioInputs] = useState>( + [] + ); + useEffect(() => { + if (ongoingCallPubkey === selectedConversationKey) { + CallManager.addVideoEventsListener(uniqueId, (options: CallManagerOptionsType) => { + const { + audioInputsList, + camerasList, + isLocalVideoStreamMuted, + isRemoteVideoStreamMuted, + localStream: lLocalStream, + remoteStream: lRemoteStream, + isAudioMuted, + } = options; + if (mountedState()) { + setLocalStream(lLocalStream); + setRemoteStream(lRemoteStream); + setRemoteStreamVideoIsMuted(isRemoteVideoStreamMuted); + setLocalStreamVideoIsMuted(isLocalVideoStreamMuted); + setOurAudioIsMuted(isAudioMuted); + + setCurrentConnectedCameras(camerasList); + setCurrentConnectedAudioInputs(audioInputsList); + } + }); + } + + return () => { + CallManager.removeVideoEventsListener(uniqueId); + }; + }, [ongoingCallPubkey, selectedConversationKey, isFullScreen]); + + return { + currentConnectedAudioInputs, + currentConnectedCameras, + localStreamVideoIsMuted, + remoteStreamVideoIsMuted, + localStream, + remoteStream, + isAudioMuted: ourAudioIsMuted, + }; +} diff --git a/ts/session/utils/CallManager.ts b/ts/session/utils/CallManager.ts index fec70d7ae..1f2295aa4 100644 --- a/ts/session/utils/CallManager.ts +++ b/ts/session/utils/CallManager.ts @@ -9,6 +9,7 @@ import { callConnected, endCall, incomingCall, + setFullScreenCall, startingCallWith, } from '../../state/ducks/conversations'; import { getConversationController } from '../conversations'; @@ -29,27 +30,44 @@ export type CallManagerOptionsType = { audioInputsList: Array; isLocalVideoStreamMuted: boolean; isRemoteVideoStreamMuted: boolean; + isAudioMuted: boolean; }; export type CallManagerListener = ((options: CallManagerOptionsType) => void) | null; -let videoEventsListener: CallManagerListener; - -function callVideoListener() { - if (videoEventsListener) { - videoEventsListener({ - localStream: mediaDevices, - remoteStream, - camerasList, - audioInputsList, - isRemoteVideoStreamMuted: remoteVideoStreamIsMuted, - isLocalVideoStreamMuted: selectedCameraId === INPUT_DISABLED_DEVICE_ID, +const videoEventsListeners: Array<{ id: string; listener: CallManagerListener }> = []; + +function callVideoListeners() { + if (videoEventsListeners.length) { + videoEventsListeners.forEach(item => { + item.listener?.({ + localStream: mediaDevices, + remoteStream, + camerasList, + audioInputsList, + isRemoteVideoStreamMuted: remoteVideoStreamIsMuted, + isLocalVideoStreamMuted: selectedCameraId === INPUT_DISABLED_DEVICE_ID, + isAudioMuted: selectedAudioInputId === INPUT_DISABLED_DEVICE_ID, + }); }); } } -export function setVideoEventsListener(listener: CallManagerListener) { - videoEventsListener = listener; - callVideoListener(); +export function addVideoEventsListener(uniqueId: string, listener: CallManagerListener) { + const indexFound = videoEventsListeners.findIndex(m => m.id === uniqueId); + if (indexFound === -1) { + videoEventsListeners.push({ id: uniqueId, listener }); + } else { + videoEventsListeners[indexFound].listener = listener; + } + callVideoListeners(); +} + +export function removeVideoEventsListener(uniqueId: string) { + const indexFound = videoEventsListeners.findIndex(m => m.id === uniqueId); + if (indexFound !== -1) { + videoEventsListeners.splice(indexFound); + } + callVideoListeners(); } /** @@ -82,7 +100,7 @@ const configuration: RTCConfiguration = { }; let selectedCameraId: string = INPUT_DISABLED_DEVICE_ID; -let selectedAudioInputId: string | undefined; +let selectedAudioInputId: string = INPUT_DISABLED_DEVICE_ID; let camerasList: Array = []; let audioInputsList: Array = []; @@ -96,7 +114,7 @@ async function getConnectedDevices(type: 'videoinput' | 'audioinput') { if (typeof navigator !== 'undefined') { navigator.mediaDevices.addEventListener('devicechange', async () => { await updateInputLists(); - callVideoListener(); + callVideoListeners(); }); } async function updateInputLists() { @@ -137,7 +155,7 @@ export async function selectCameraByDeviceId(cameraDeviceId: string) { sender.track.enabled = false; } sendVideoStatusViaDataChannel(); - callVideoListener(); + callVideoListeners(); return; } if (camerasList.some(m => m.deviceId === cameraDeviceId)) { @@ -168,13 +186,13 @@ export async function selectCameraByDeviceId(cameraDeviceId: string) { mediaDevices?.addTrack(videoTrack); sendVideoStatusViaDataChannel(); - callVideoListener(); + callVideoListeners(); } else { throw new Error('Failed to get sender for selectCameraByDeviceId '); } } catch (e) { window.log.warn('selectCameraByDeviceId failed with', e.message); - callVideoListener(); + callVideoListeners(); } } } @@ -188,6 +206,7 @@ export async function selectAudioInputByDeviceId(audioInputDeviceId: string) { if (sender?.track) { sender.track.enabled = false; } + callVideoListeners(); return; } if (audioInputsList.some(m => m.deviceId === audioInputDeviceId)) { @@ -218,6 +237,8 @@ export async function selectAudioInputByDeviceId(audioInputDeviceId: string) { } catch (e) { window.log.warn('selectAudioInputByDeviceId failed with', e.message); } + + callVideoListeners(); } } @@ -277,20 +298,20 @@ async function openMediaDevicesAndAddTracks() { return; } - const firstAudio = audioInputsList[0].deviceId; - const firstVideo = camerasList[0].deviceId; + selectedAudioInputId = audioInputsList[0].deviceId; + selectedCameraId = INPUT_DISABLED_DEVICE_ID; window.log.info( - `openMediaDevices videoDevice:${firstVideo}:${camerasList[0].label} audioDevice:${firstAudio}` + `openMediaDevices videoDevice:${selectedCameraId}:${camerasList[0].label} audioDevice:${selectedAudioInputId}` ); const devicesConfig = { audio: { - deviceId: firstAudio, + deviceId: selectedAudioInputId, echoCancellation: true, }, video: { - deviceId: firstVideo, + deviceId: selectedCameraId, // width: VIDEO_WIDTH, // height: Math.floor(VIDEO_WIDTH * VIDEO_RATIO), }, @@ -309,7 +330,7 @@ async function openMediaDevicesAndAddTracks() { ToastUtils.pushVideoCallPermissionNeeded(); closeVideoCall(); } - callVideoListener(); + callVideoListeners(); } // tslint:disable-next-line: function-name @@ -426,16 +447,9 @@ function closeVideoCall() { mediaDevices = null; remoteStream = null; selectedCameraId = INPUT_DISABLED_DEVICE_ID; - if (videoEventsListener) { - videoEventsListener({ - audioInputsList: [], - camerasList: [], - isLocalVideoStreamMuted: true, - isRemoteVideoStreamMuted: true, - localStream: null, - remoteStream: null, - }); - } + selectedAudioInputId = INPUT_DISABLED_DEVICE_ID; + callVideoListeners(); + window.inboxStore?.dispatch(setFullScreenCall(false)); } function onDataChannelReceivedMessage(ev: MessageEvent) { @@ -445,7 +459,7 @@ function onDataChannelReceivedMessage(ev: MessageEvent) { if (parsed.video !== undefined) { remoteVideoStreamIsMuted = !Boolean(parsed.video); } - callVideoListener(); + callVideoListeners(); } catch (e) { window.log.warn('onDataChannelReceivedMessage Could not parse data in event', ev); } @@ -489,11 +503,11 @@ function createOrGetPeerConnection(withPubkey: string, createDataChannel: boolea peerConnection.ontrack = event => { event.track.onunmute = () => { remoteStream?.addTrack(event.track); - callVideoListener(); + callVideoListeners(); }; event.track.onmute = () => { remoteStream?.removeTrack(event.track); - callVideoListener(); + callVideoListeners(); }; }; peerConnection.onconnectionstatechange = () => { @@ -604,16 +618,7 @@ export function handleCallTypeEndCall(sender: string) { // we just got a end call event from whoever we are in a call with if (callingConvos.length === 1 && callingConvos[0].id === sender) { closeVideoCall(); - if (videoEventsListener) { - videoEventsListener({ - audioInputsList: [], - camerasList: [], - isLocalVideoStreamMuted: true, - isRemoteVideoStreamMuted: true, - localStream: null, - remoteStream: null, - }); - } + window.inboxStore?.dispatch(endCall({ pubkey: sender })); } } diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 9143a3349..90bccae5e 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -277,6 +277,7 @@ export type ConversationsStateType = { quotedMessage?: ReplyingToMessageProps; areMoreMessagesBeingFetched: boolean; haveDoneFirstScroll: boolean; + callIsInFullScreen: boolean; showScrollButton: boolean; animateQuotedMessageId?: string; @@ -371,6 +372,7 @@ export function getEmptyConversationState(): ConversationsStateType { mentionMembers: [], firstUnreadMessageId: undefined, haveDoneFirstScroll: false, + callIsInFullScreen: false, }; } @@ -696,6 +698,8 @@ const conversationsSlice = createSlice({ return { conversationLookup: state.conversationLookup, + callIsInFullScreen: state.callIsInFullScreen, + selectedConversation: action.payload.id, areMoreMessagesBeingFetched: false, messages: action.payload.initialMessages, @@ -850,6 +854,10 @@ const conversationsSlice = createSlice({ void foundConvo.commit(); return state; }, + setFullScreenCall(state: ConversationsStateType, action: PayloadAction) { + state.callIsInFullScreen = action.payload; + return state; + }, }, extraReducers: (builder: any) => { // Add reducers for additional action types here, and handle loading state as needed @@ -915,6 +923,7 @@ export const { answerCall, callConnected, startingCallWith, + setFullScreenCall, } = actions; export async function openConversationWithMessages(args: { diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 41f1cdd0c..e6384037d 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -122,6 +122,16 @@ export const getHasOngoingCall = createSelector( (withConvo: ReduxConversationType | undefined): boolean => !!withConvo ); +export const getHasOngoingCallWithPubkey = createSelector( + getHasOngoingCallWith, + (withConvo: ReduxConversationType | undefined): string | undefined => withConvo?.id +); + +export const getCallIsInFullScreen = createSelector( + getConversations, + (state: ConversationsStateType): boolean => state.callIsInFullScreen +); + /** * Returns true if the current conversation selected is a group conversation. * Returns false if the current conversation selected is not a group conversation, or none are selected