blob: 13a30a26de40d4d8aaf87b5a696fbe140a9af99a [file] [log] [blame]
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -05001/*
2 * Copyright (C) 2022 Savoir-faire Linux Inc.
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU Affero General Public License as
6 * published by the Free Software Foundation; either version 3 of the
7 * License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU Affero General Public License for more details.
13 *
14 * You should have received a copy of the GNU Affero General Public
15 * License along with this program. If not, see
16 * <https://www.gnu.org/licenses/>.
17 */
18
19import { WebRtcIceCandidate, WebRtcSdp, WebSocketMessageType } from 'jami-web-common';
20import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
21
22import { WithChildren } from '../utils/utils';
simon71d1c0a2022-11-24 15:28:33 -050023import { useAuthContext } from './AuthProvider';
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050024import { ConversationContext } from './ConversationProvider';
25import { WebSocketContext } from './WebSocketProvider';
26
27interface IWebRtcContext {
28 isConnected: boolean;
29
30 remoteStreams: readonly MediaStream[] | undefined;
31 webRtcConnection: RTCPeerConnection | undefined;
32
33 sendWebRtcOffer: (sdp: RTCSessionDescriptionInit) => Promise<void>;
34}
35
36const defaultWebRtcContext: IWebRtcContext = {
37 isConnected: false,
38 remoteStreams: undefined,
39 webRtcConnection: undefined,
40 sendWebRtcOffer: async () => {},
41};
42
43export const WebRtcContext = createContext<IWebRtcContext>(defaultWebRtcContext);
44
45export default ({ children }: WithChildren) => {
simon71d1c0a2022-11-24 15:28:33 -050046 const { account } = useAuthContext();
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050047 const webSocket = useContext(WebSocketContext);
Charliec18d6402022-11-27 13:01:04 -050048 const { conversation, conversationId } = useContext(ConversationContext);
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050049 const [webRtcConnection, setWebRtcConnection] = useState<RTCPeerConnection | undefined>();
50 const [remoteStreams, setRemoteStreams] = useState<readonly MediaStream[]>();
51 const [isConnected, setIsConnected] = useState(false);
52
53 // TODO: This logic will have to change to support multiple people in a call
54 const contactUri = useMemo(() => conversation.getFirstMember().contact.getUri(), [conversation]);
55
56 useEffect(() => {
simon71d1c0a2022-11-24 15:28:33 -050057 if (!webRtcConnection && account) {
58 const iceServers: RTCIceServer[] = [];
59
60 if (account.getDetails()['TURN.enable'] === 'true') {
61 iceServers.push({
62 urls: 'turn:' + account.getDetails()['TURN.server'],
63 username: account.getDetails()['TURN.username'],
64 credential: account.getDetails()['TURN.password'],
65 });
66 }
67
68 if (account.getDetails()['STUN.enable'] === 'true') {
69 iceServers.push({
70 urls: 'stun:' + account.getDetails()['STUN.server'],
71 });
72 }
73
74 setWebRtcConnection(new RTCPeerConnection({ iceServers: iceServers }));
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050075 }
simon71d1c0a2022-11-24 15:28:33 -050076 }, [account, webRtcConnection]);
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050077
78 const sendWebRtcOffer = useCallback(
79 async (sdp: RTCSessionDescriptionInit) => {
80 if (!webRtcConnection || !webSocket) {
81 throw new Error('Could not send WebRTC offer');
82 }
83
84 const webRtcOffer: WebRtcSdp = {
85 contactId: contactUri,
Charliec18d6402022-11-27 13:01:04 -050086 conversationId: conversationId,
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050087 sdp,
88 };
89
simonfeaa1db2022-11-26 20:13:18 -050090 await webRtcConnection.setLocalDescription(new RTCSessionDescription(sdp));
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050091 console.info('Sending WebRtcOffer', webRtcOffer);
92 webSocket.send(WebSocketMessageType.WebRtcOffer, webRtcOffer);
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050093 },
Charliec18d6402022-11-27 13:01:04 -050094 [webRtcConnection, webSocket, conversationId, contactUri]
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050095 );
96
97 const sendWebRtcAnswer = useCallback(
98 (sdp: RTCSessionDescriptionInit) => {
99 if (!webRtcConnection || !webSocket) {
100 throw new Error('Could not send WebRTC answer');
101 }
102
103 const webRtcAnswer: WebRtcSdp = {
104 contactId: contactUri,
Charliec18d6402022-11-27 13:01:04 -0500105 conversationId: conversationId,
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500106 sdp,
107 };
108
109 console.info('Sending WebRtcAnswer', webRtcAnswer);
110 webSocket.send(WebSocketMessageType.WebRtcAnswer, webRtcAnswer);
111 },
Charliec18d6402022-11-27 13:01:04 -0500112 [contactUri, conversationId, webRtcConnection, webSocket]
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500113 );
114
115 useEffect(() => {
116 if (!webSocket || !webRtcConnection) {
117 return;
118 }
119
120 const webRtcOfferListener = async (data: WebRtcSdp) => {
121 console.info('Received event on WebRtcOffer', data);
Charliec18d6402022-11-27 13:01:04 -0500122 if (data.conversationId !== conversationId) {
123 console.warn('Wrong incoming conversationId, ignoring action');
124 return;
125 }
126
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500127 await webRtcConnection.setRemoteDescription(new RTCSessionDescription(data.sdp));
128
129 const sdp = await webRtcConnection.createAnswer({
130 offerToReceiveAudio: true,
131 offerToReceiveVideo: true,
132 });
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500133 await webRtcConnection.setLocalDescription(new RTCSessionDescription(sdp));
simonfeaa1db2022-11-26 20:13:18 -0500134 sendWebRtcAnswer(sdp);
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500135 };
136
137 const webRtcAnswerListener = async (data: WebRtcSdp) => {
138 console.info('Received event on WebRtcAnswer', data);
Charliec18d6402022-11-27 13:01:04 -0500139 if (data.conversationId !== conversationId) {
140 console.warn('Wrong incoming conversationId, ignoring action');
141 return;
142 }
143
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500144 await webRtcConnection.setRemoteDescription(new RTCSessionDescription(data.sdp));
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500145 };
146
147 const webRtcIceCandidateListener = async (data: WebRtcIceCandidate) => {
148 console.info('Received event on WebRtcIceCandidate', data);
Charliec18d6402022-11-27 13:01:04 -0500149 if (data.conversationId !== conversationId) {
150 console.warn('Wrong incoming conversationId, ignoring action');
151 return;
152 }
153
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500154 await webRtcConnection.addIceCandidate(data.candidate);
155 };
156
157 webSocket.bind(WebSocketMessageType.WebRtcOffer, webRtcOfferListener);
158 webSocket.bind(WebSocketMessageType.WebRtcAnswer, webRtcAnswerListener);
159 webSocket.bind(WebSocketMessageType.WebRtcIceCandidate, webRtcIceCandidateListener);
160
161 return () => {
162 webSocket.unbind(WebSocketMessageType.WebRtcOffer, webRtcOfferListener);
163 webSocket.unbind(WebSocketMessageType.WebRtcAnswer, webRtcAnswerListener);
164 webSocket.unbind(WebSocketMessageType.WebRtcIceCandidate, webRtcIceCandidateListener);
165 };
Charliec18d6402022-11-27 13:01:04 -0500166 }, [webSocket, webRtcConnection, sendWebRtcAnswer, conversationId]);
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500167
168 useEffect(() => {
169 if (!webRtcConnection || !webSocket) {
170 return;
171 }
172
173 const iceCandidateEventListener = (event: RTCPeerConnectionIceEvent) => {
174 console.info('Received WebRTC event on icecandidate', event);
175 if (!contactUri) {
176 throw new Error('Could not handle WebRTC event on icecandidate: contactUri is not defined');
177 }
178
179 if (event.candidate) {
180 const webRtcIceCandidate: WebRtcIceCandidate = {
181 contactId: contactUri,
Charliec18d6402022-11-27 13:01:04 -0500182 conversationId: conversationId,
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500183 candidate: event.candidate,
184 };
185
186 console.info('Sending WebRtcIceCandidate', webRtcIceCandidate);
187 webSocket.send(WebSocketMessageType.WebRtcIceCandidate, webRtcIceCandidate);
188 }
189 };
190
191 const trackEventListener = (event: RTCTrackEvent) => {
192 console.info('Received WebRTC event on track', event);
193 setRemoteStreams(event.streams);
194 };
195
MichelleSS55164202022-11-25 18:36:14 -0500196 const iceConnectionStateChangeEventListener = () => {
simonfeaa1db2022-11-26 20:13:18 -0500197 setIsConnected(
MichelleSS55164202022-11-25 18:36:14 -0500198 webRtcConnection.iceConnectionState === 'connected' || webRtcConnection.iceConnectionState === 'completed'
simonfeaa1db2022-11-26 20:13:18 -0500199 );
200 };
201
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500202 webRtcConnection.addEventListener('icecandidate', iceCandidateEventListener);
203 webRtcConnection.addEventListener('track', trackEventListener);
MichelleSS55164202022-11-25 18:36:14 -0500204 webRtcConnection.addEventListener('iceconnectionstatechange', iceConnectionStateChangeEventListener);
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500205
206 return () => {
207 webRtcConnection.removeEventListener('icecandidate', iceCandidateEventListener);
208 webRtcConnection.removeEventListener('track', trackEventListener);
MichelleSS55164202022-11-25 18:36:14 -0500209 webRtcConnection.removeEventListener('iceconnectionstatechange', iceConnectionStateChangeEventListener);
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500210 };
Charliec18d6402022-11-27 13:01:04 -0500211 }, [webRtcConnection, webSocket, contactUri, conversationId]);
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -0500212
213 return (
214 <WebRtcContext.Provider
215 value={{
216 isConnected,
217 remoteStreams,
218 webRtcConnection,
219 sendWebRtcOffer,
220 }}
221 >
222 {children}
223 </WebRtcContext.Provider>
224 );
225};