Bind call buttons and set ringing timeout
- Accept call with or without video
- Ringing / connection timeout
- Close camera when call ends
- Display incoming call type (audio vs video)
GitLab: #154
GitLab: #165
GitLab: #168
Change-Id: I93ba7148941656b5bebd3ca38898bce0d4db41ca
diff --git a/client/src/components/CallButtons.tsx b/client/src/components/CallButtons.tsx
index 6e77b39..f6f7275 100644
--- a/client/src/components/CallButtons.tsx
+++ b/client/src/components/CallButtons.tsx
@@ -284,7 +284,7 @@
<ColoredCallButton
aria-label="answer call audio"
onClick={() => {
- acceptCall();
+ acceptCall(false);
}}
Icon={PlaceAudioCallIcon}
paletteColor={(theme) => theme.palette.success}
@@ -300,7 +300,7 @@
<ColoredCallButton
aria-label="answer call video"
onClick={() => {
- acceptCall();
+ acceptCall(true);
}}
paletteColor={(theme) => theme.palette.success}
Icon={VideoCameraIcon}
diff --git a/client/src/contexts/CallProvider.tsx b/client/src/contexts/CallProvider.tsx
index a4a9ead..6b8d4ee 100644
--- a/client/src/contexts/CallProvider.tsx
+++ b/client/src/contexts/CallProvider.tsx
@@ -15,12 +15,13 @@
* License along with this program. If not, see
* <https://www.gnu.org/licenses/>.
*/
-import { CallAction, WebSocketMessageType } from 'jami-web-common';
+import { CallAction, CallBegin, WebSocketMessageType } from 'jami-web-common';
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { Navigate, useNavigate } from 'react-router-dom';
import { useUrlParams } from '../hooks/useUrlParams';
import { CallRouteParams } from '../router';
+import { callTimeoutMs } from '../utils/constants';
import { SetState, WithChildren } from '../utils/utils';
import { ConversationContext } from './ConversationProvider';
import { WebRtcContext } from './WebRtcProvider';
@@ -53,7 +54,7 @@
callStatus: CallStatus;
callStartTime: Date | undefined;
- acceptCall: () => void;
+ acceptCall: (withVideoOn: boolean) => void;
endCall: () => void;
}
@@ -79,7 +80,7 @@
callStatus: CallStatus.Default,
callStartTime: undefined,
- acceptCall: () => {},
+ acceptCall: (_: boolean) => {},
endCall: () => {},
};
@@ -163,22 +164,54 @@
}
}, [localStream, webRtcConnection]);
+ const setAudioStatus = useCallback(
+ (isOn: boolean) => {
+ if (!localStream) {
+ return;
+ }
+
+ for (const track of localStream.getAudioTracks()) {
+ track.enabled = isOn;
+ }
+
+ setIsAudioOn(isOn);
+ },
+ [localStream]
+ );
+
+ const setVideoStatus = useCallback(
+ (isOn: boolean) => {
+ if (!localStream) {
+ return;
+ }
+
+ for (const track of localStream.getVideoTracks()) {
+ track.enabled = isOn;
+ }
+
+ setIsVideoOn(isOn);
+ },
+ [localStream]
+ );
+
useEffect(() => {
if (!webSocket) {
return;
}
if (callRole === 'caller' && callStatus === CallStatus.Default) {
- const callBegin: CallAction = {
+ const callBegin: CallBegin = {
contactId: contactUri,
conversationId,
+ withVideoOn: routeState?.isVideoOn ?? false,
};
console.info('Sending CallBegin', callBegin);
webSocket.send(WebSocketMessageType.CallBegin, callBegin);
setCallStatus(CallStatus.Ringing);
+ setIsVideoOn(routeState?.isVideoOn ?? false);
}
- }, [webSocket, callRole, callStatus, contactUri, conversationId]);
+ }, [webSocket, callRole, callStatus, contactUri, conversationId, routeState]);
useEffect(() => {
const onFullscreenChange = () => {
@@ -224,9 +257,16 @@
throw new Error('Could not quit call: webRtcConnection is not defined');
}
+ const localTracks = localStream?.getTracks();
+ if (localTracks) {
+ for (const track of localTracks) {
+ track.stop();
+ }
+ }
+
webRtcConnection.close();
navigate(`/conversation/${conversationId}`);
- }, [webRtcConnection, navigate, conversationId]);
+ }, [webRtcConnection, localStream, navigate, conversationId]);
useEffect(() => {
if (!webSocket) {
@@ -249,24 +289,29 @@
if (callStatus === CallStatus.Connecting && isConnected) {
console.info('Changing call status to InCall');
setCallStatus(CallStatus.InCall);
+ setVideoStatus(isVideoOn);
setCallStartTime(new Date());
}
- }, [isConnected, callStatus]);
+ }, [isConnected, callStatus, setVideoStatus, isVideoOn]);
- const acceptCall = useCallback(() => {
- if (!webSocket) {
- throw new Error('Could not accept call');
- }
+ const acceptCall = useCallback(
+ (withVideoOn: boolean) => {
+ if (!webSocket) {
+ throw new Error('Could not accept call');
+ }
- const callAccept: CallAction = {
- contactId: contactUri,
- conversationId,
- };
+ const callAccept: CallAction = {
+ contactId: contactUri,
+ conversationId,
+ };
- console.info('Sending CallAccept', callAccept);
- webSocket.send(WebSocketMessageType.CallAccept, callAccept);
- setCallStatus(CallStatus.Connecting);
- }, [webSocket, contactUri, conversationId]);
+ console.info('Sending CallAccept', callAccept);
+ webSocket.send(WebSocketMessageType.CallAccept, callAccept);
+ setIsVideoOn(withVideoOn);
+ setCallStatus(CallStatus.Connecting);
+ },
+ [webSocket, contactUri, conversationId]
+ );
const endCall = useCallback(() => {
if (!webSocket) {
@@ -284,35 +329,18 @@
// TODO: write in chat that the call ended
}, [webSocket, contactUri, conversationId, quitCall]);
- const setAudioStatus = useCallback(
- (isOn: boolean) => {
- if (!localStream) {
- return;
+ useEffect(() => {
+ const checkStatusTimeout = () => {
+ if (callStatus !== CallStatus.InCall) {
+ endCall();
}
+ };
+ const timeoutId = setTimeout(checkStatusTimeout, callTimeoutMs);
- for (const track of localStream.getAudioTracks()) {
- track.enabled = isOn;
- }
-
- setIsAudioOn(isOn);
- },
- [localStream]
- );
-
- const setVideoStatus = useCallback(
- (isOn: boolean) => {
- if (!localStream) {
- return;
- }
-
- for (const track of localStream.getVideoTracks()) {
- track.enabled = isOn;
- }
-
- setIsVideoOn(isOn);
- },
- [localStream]
- );
+ return () => {
+ clearTimeout(timeoutId);
+ };
+ }, [callStatus, endCall]);
if (!callRole || callStatus === undefined) {
console.error('Invalid route. Redirecting...');
diff --git a/client/src/contexts/WebRtcProvider.tsx b/client/src/contexts/WebRtcProvider.tsx
index f87e1ff..f1c9add 100644
--- a/client/src/contexts/WebRtcProvider.tsx
+++ b/client/src/contexts/WebRtcProvider.tsx
@@ -175,20 +175,20 @@
setRemoteStreams(event.streams);
};
- const connectionStateChangeEventListener = () => {
+ const iceConnectionStateChangeEventListener = () => {
setIsConnected(
- webRtcConnection.iceConnectionState === 'completed' || webRtcConnection.iceConnectionState === 'connected'
+ webRtcConnection.iceConnectionState === 'connected' || webRtcConnection.iceConnectionState === 'completed'
);
};
webRtcConnection.addEventListener('icecandidate', iceCandidateEventListener);
webRtcConnection.addEventListener('track', trackEventListener);
- webRtcConnection.addEventListener('iceconnectionstatechange', connectionStateChangeEventListener);
+ webRtcConnection.addEventListener('iceconnectionstatechange', iceConnectionStateChangeEventListener);
return () => {
webRtcConnection.removeEventListener('icecandidate', iceCandidateEventListener);
webRtcConnection.removeEventListener('track', trackEventListener);
- webRtcConnection.removeEventListener('iceconnectionstatechange', connectionStateChangeEventListener);
+ webRtcConnection.removeEventListener('iceconnectionstatechange', iceConnectionStateChangeEventListener);
};
}, [webRtcConnection, webSocket, contactUri]);
diff --git a/client/src/managers/NotificationManager.tsx b/client/src/managers/NotificationManager.tsx
index dacc3c1..5362f54 100644
--- a/client/src/managers/NotificationManager.tsx
+++ b/client/src/managers/NotificationManager.tsx
@@ -15,7 +15,7 @@
* License along with this program. If not, see
* <https://www.gnu.org/licenses/>.
*/
-import { CallAction, WebSocketMessageType } from 'jami-web-common';
+import { CallBegin, WebSocketMessageType } from 'jami-web-common';
import { useContext, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
@@ -37,11 +37,12 @@
return;
}
- const callBeginListener = (data: CallAction) => {
+ const callBeginListener = (data: CallBegin) => {
console.info('Received event on CallBegin', data);
navigate(`/conversation/${data.conversationId}/call?role=receiver`, {
state: {
callStatus: CallStatus.Ringing,
+ isVideoOn: data.withVideoOn,
},
});
};
diff --git a/client/src/pages/CallInterface.tsx b/client/src/pages/CallInterface.tsx
index c0ab334..11a1df5 100644
--- a/client/src/pages/CallInterface.tsx
+++ b/client/src/pages/CallInterface.tsx
@@ -29,6 +29,7 @@
useState,
} from 'react';
import Draggable from 'react-draggable';
+import { useLocation } from 'react-router-dom';
import { ExpandableButtonProps } from '../components/Button';
import {
@@ -51,6 +52,7 @@
export default () => {
const { callRole, callStatus, isChatShown, isFullscreen } = useContext(CallContext);
const callInterfaceRef = useRef<HTMLDivElement>();
+ const { state } = useLocation();
useEffect(() => {
if (!callInterfaceRef.current) {
@@ -69,7 +71,7 @@
<CallPending
pending={callRole}
caller={callStatus === CallStatus.Connecting ? 'connecting' : 'calling'}
- medium="audio"
+ medium={state?.isVideoOn ? 'video' : 'audio'}
/>
);
}
diff --git a/client/src/utils/constants.ts b/client/src/utils/constants.ts
index 56c738a..d81a145 100644
--- a/client/src/utils/constants.ts
+++ b/client/src/utils/constants.ts
@@ -21,6 +21,8 @@
export const jamiLogoDefaultSize = '512px';
+export const callTimeoutMs = 60_000;
+
const apiUrl: string = import.meta.env.VITE_API_URL;
if (!apiUrl) {