blob: a56efab84a47bdf919d321723e264576c9d82b03 [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);
48 const { conversation } = useContext(ConversationContext);
49 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,
86 sdp,
87 };
88
89 console.info('Sending WebRtcOffer', webRtcOffer);
90 webSocket.send(WebSocketMessageType.WebRtcOffer, webRtcOffer);
91 await webRtcConnection.setLocalDescription(new RTCSessionDescription(sdp));
92 },
93 [webRtcConnection, webSocket, contactUri]
94 );
95
96 const sendWebRtcAnswer = useCallback(
97 (sdp: RTCSessionDescriptionInit) => {
98 if (!webRtcConnection || !webSocket) {
99 throw new Error('Could not send WebRTC answer');
100 }
101
102 const webRtcAnswer: WebRtcSdp = {
103 contactId: contactUri,
104 sdp,
105 };
106
107 console.info('Sending WebRtcAnswer', webRtcAnswer);
108 webSocket.send(WebSocketMessageType.WebRtcAnswer, webRtcAnswer);
109 },
110 [contactUri, webRtcConnection, webSocket]
111 );
112
113 useEffect(() => {
114 if (!webSocket || !webRtcConnection) {
115 return;
116 }
117
118 const webRtcOfferListener = async (data: WebRtcSdp) => {
119 console.info('Received event on WebRtcOffer', data);
120 await webRtcConnection.setRemoteDescription(new RTCSessionDescription(data.sdp));
121
122 const sdp = await webRtcConnection.createAnswer({
123 offerToReceiveAudio: true,
124 offerToReceiveVideo: true,
125 });
126 sendWebRtcAnswer(sdp);
127 await webRtcConnection.setLocalDescription(new RTCSessionDescription(sdp));
128 setIsConnected(true);
129 };
130
131 const webRtcAnswerListener = async (data: WebRtcSdp) => {
132 console.info('Received event on WebRtcAnswer', data);
133 await webRtcConnection.setRemoteDescription(new RTCSessionDescription(data.sdp));
134 setIsConnected(true);
135 };
136
137 const webRtcIceCandidateListener = async (data: WebRtcIceCandidate) => {
138 console.info('Received event on WebRtcIceCandidate', data);
139 await webRtcConnection.addIceCandidate(data.candidate);
140 };
141
142 webSocket.bind(WebSocketMessageType.WebRtcOffer, webRtcOfferListener);
143 webSocket.bind(WebSocketMessageType.WebRtcAnswer, webRtcAnswerListener);
144 webSocket.bind(WebSocketMessageType.WebRtcIceCandidate, webRtcIceCandidateListener);
145
146 return () => {
147 webSocket.unbind(WebSocketMessageType.WebRtcOffer, webRtcOfferListener);
148 webSocket.unbind(WebSocketMessageType.WebRtcAnswer, webRtcAnswerListener);
149 webSocket.unbind(WebSocketMessageType.WebRtcIceCandidate, webRtcIceCandidateListener);
150 };
151 }, [webSocket, webRtcConnection, sendWebRtcAnswer]);
152
153 useEffect(() => {
154 if (!webRtcConnection || !webSocket) {
155 return;
156 }
157
158 const iceCandidateEventListener = (event: RTCPeerConnectionIceEvent) => {
159 console.info('Received WebRTC event on icecandidate', event);
160 if (!contactUri) {
161 throw new Error('Could not handle WebRTC event on icecandidate: contactUri is not defined');
162 }
163
164 if (event.candidate) {
165 const webRtcIceCandidate: WebRtcIceCandidate = {
166 contactId: contactUri,
167 candidate: event.candidate,
168 };
169
170 console.info('Sending WebRtcIceCandidate', webRtcIceCandidate);
171 webSocket.send(WebSocketMessageType.WebRtcIceCandidate, webRtcIceCandidate);
172 }
173 };
174
175 const trackEventListener = (event: RTCTrackEvent) => {
176 console.info('Received WebRTC event on track', event);
177 setRemoteStreams(event.streams);
178 };
179
180 webRtcConnection.addEventListener('icecandidate', iceCandidateEventListener);
181 webRtcConnection.addEventListener('track', trackEventListener);
182
183 return () => {
184 webRtcConnection.removeEventListener('icecandidate', iceCandidateEventListener);
185 webRtcConnection.removeEventListener('track', trackEventListener);
186 };
187 }, [webRtcConnection, webSocket, contactUri]);
188
189 return (
190 <WebRtcContext.Provider
191 value={{
192 isConnected,
193 remoteStreams,
194 webRtcConnection,
195 sendWebRtcOffer,
196 }}
197 >
198 {children}
199 </WebRtcContext.Provider>
200 );
201};