blob: 39992598726ca32362a89690bea8e166d978b4c7 [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';
23import { ConversationContext } from './ConversationProvider';
24import { WebSocketContext } from './WebSocketProvider';
25
26interface IWebRtcContext {
27 isConnected: boolean;
28
29 remoteStreams: readonly MediaStream[] | undefined;
30 webRtcConnection: RTCPeerConnection | undefined;
31
32 sendWebRtcOffer: (sdp: RTCSessionDescriptionInit) => Promise<void>;
33}
34
35const defaultWebRtcContext: IWebRtcContext = {
36 isConnected: false,
37 remoteStreams: undefined,
38 webRtcConnection: undefined,
39 sendWebRtcOffer: async () => {},
40};
41
42export const WebRtcContext = createContext<IWebRtcContext>(defaultWebRtcContext);
43
44export default ({ children }: WithChildren) => {
45 const webSocket = useContext(WebSocketContext);
46 const { conversation } = useContext(ConversationContext);
47 const [webRtcConnection, setWebRtcConnection] = useState<RTCPeerConnection | undefined>();
48 const [remoteStreams, setRemoteStreams] = useState<readonly MediaStream[]>();
49 const [isConnected, setIsConnected] = useState(false);
50
51 // TODO: This logic will have to change to support multiple people in a call
52 const contactUri = useMemo(() => conversation.getFirstMember().contact.getUri(), [conversation]);
53
54 useEffect(() => {
55 if (!webRtcConnection) {
56 // TODO: Use SFL iceServers
57 const iceConfig = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] };
58 setWebRtcConnection(new RTCPeerConnection(iceConfig));
59 }
60 }, [webRtcConnection]);
61
62 const sendWebRtcOffer = useCallback(
63 async (sdp: RTCSessionDescriptionInit) => {
64 if (!webRtcConnection || !webSocket) {
65 throw new Error('Could not send WebRTC offer');
66 }
67
68 const webRtcOffer: WebRtcSdp = {
69 contactId: contactUri,
70 sdp,
71 };
72
73 console.info('Sending WebRtcOffer', webRtcOffer);
74 webSocket.send(WebSocketMessageType.WebRtcOffer, webRtcOffer);
75 await webRtcConnection.setLocalDescription(new RTCSessionDescription(sdp));
76 },
77 [webRtcConnection, webSocket, contactUri]
78 );
79
80 const sendWebRtcAnswer = useCallback(
81 (sdp: RTCSessionDescriptionInit) => {
82 if (!webRtcConnection || !webSocket) {
83 throw new Error('Could not send WebRTC answer');
84 }
85
86 const webRtcAnswer: WebRtcSdp = {
87 contactId: contactUri,
88 sdp,
89 };
90
91 console.info('Sending WebRtcAnswer', webRtcAnswer);
92 webSocket.send(WebSocketMessageType.WebRtcAnswer, webRtcAnswer);
93 },
94 [contactUri, webRtcConnection, webSocket]
95 );
96
97 useEffect(() => {
98 if (!webSocket || !webRtcConnection) {
99 return;
100 }
101
102 const webRtcOfferListener = async (data: WebRtcSdp) => {
103 console.info('Received event on WebRtcOffer', data);
104 await webRtcConnection.setRemoteDescription(new RTCSessionDescription(data.sdp));
105
106 const sdp = await webRtcConnection.createAnswer({
107 offerToReceiveAudio: true,
108 offerToReceiveVideo: true,
109 });
110 sendWebRtcAnswer(sdp);
111 await webRtcConnection.setLocalDescription(new RTCSessionDescription(sdp));
112 setIsConnected(true);
113 };
114
115 const webRtcAnswerListener = async (data: WebRtcSdp) => {
116 console.info('Received event on WebRtcAnswer', data);
117 await webRtcConnection.setRemoteDescription(new RTCSessionDescription(data.sdp));
118 setIsConnected(true);
119 };
120
121 const webRtcIceCandidateListener = async (data: WebRtcIceCandidate) => {
122 console.info('Received event on WebRtcIceCandidate', data);
123 await webRtcConnection.addIceCandidate(data.candidate);
124 };
125
126 webSocket.bind(WebSocketMessageType.WebRtcOffer, webRtcOfferListener);
127 webSocket.bind(WebSocketMessageType.WebRtcAnswer, webRtcAnswerListener);
128 webSocket.bind(WebSocketMessageType.WebRtcIceCandidate, webRtcIceCandidateListener);
129
130 return () => {
131 webSocket.unbind(WebSocketMessageType.WebRtcOffer, webRtcOfferListener);
132 webSocket.unbind(WebSocketMessageType.WebRtcAnswer, webRtcAnswerListener);
133 webSocket.unbind(WebSocketMessageType.WebRtcIceCandidate, webRtcIceCandidateListener);
134 };
135 }, [webSocket, webRtcConnection, sendWebRtcAnswer]);
136
137 useEffect(() => {
138 if (!webRtcConnection || !webSocket) {
139 return;
140 }
141
142 const iceCandidateEventListener = (event: RTCPeerConnectionIceEvent) => {
143 console.info('Received WebRTC event on icecandidate', event);
144 if (!contactUri) {
145 throw new Error('Could not handle WebRTC event on icecandidate: contactUri is not defined');
146 }
147
148 if (event.candidate) {
149 const webRtcIceCandidate: WebRtcIceCandidate = {
150 contactId: contactUri,
151 candidate: event.candidate,
152 };
153
154 console.info('Sending WebRtcIceCandidate', webRtcIceCandidate);
155 webSocket.send(WebSocketMessageType.WebRtcIceCandidate, webRtcIceCandidate);
156 }
157 };
158
159 const trackEventListener = (event: RTCTrackEvent) => {
160 console.info('Received WebRTC event on track', event);
161 setRemoteStreams(event.streams);
162 };
163
164 webRtcConnection.addEventListener('icecandidate', iceCandidateEventListener);
165 webRtcConnection.addEventListener('track', trackEventListener);
166
167 return () => {
168 webRtcConnection.removeEventListener('icecandidate', iceCandidateEventListener);
169 webRtcConnection.removeEventListener('track', trackEventListener);
170 };
171 }, [webRtcConnection, webSocket, contactUri]);
172
173 return (
174 <WebRtcContext.Provider
175 value={{
176 isConnected,
177 remoteStreams,
178 webRtcConnection,
179 sendWebRtcOffer,
180 }}
181 >
182 {children}
183 </WebRtcContext.Provider>
184 );
185};