blob: 48013870554b45acbb0bba0f31046abb7abde0ec [file] [log] [blame]
Charliec2c012f2022-10-05 14:09:28 -04001/*
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
simona5c54ef2022-11-18 05:26:06 -050019import {
20 AccountTextMessage,
21 WebRTCAnswerMessage,
22 WebRTCIceCandidate,
23 WebRTCOfferMessage,
24 WebSocketMessageType,
25} from 'jami-web-common';
Charlie461805e2022-11-09 10:40:15 -050026import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';
Charliec2c012f2022-10-05 14:09:28 -040027
28import { WithChildren } from '../utils/utils';
Charlie461805e2022-11-09 10:40:15 -050029import { useAuthContext } from './AuthProvider';
30import { WebSocketContext } from './WebSocketProvider';
Charliec2c012f2022-10-05 14:09:28 -040031
32interface IWebRTCContext {
33 localVideoRef: React.RefObject<HTMLVideoElement> | null;
34 remoteVideoRef: React.RefObject<HTMLVideoElement> | null;
Charlie461805e2022-11-09 10:40:15 -050035
simon9a8fe202022-11-15 18:25:49 -050036 mediaDevices: Record<MediaDeviceKind, MediaDeviceInfo[]>;
37
Charlie461805e2022-11-09 10:40:15 -050038 contactId: string;
simonce2c0c42022-11-02 17:39:31 -040039
40 isAudioOn: boolean;
41 setAudioStatus: (isOn: boolean) => void;
42 isVideoOn: boolean;
43 setVideoStatus: (isOn: boolean) => void;
44 sendWebRTCOffer: () => void;
Charliec2c012f2022-10-05 14:09:28 -040045}
46
simonce2c0c42022-11-02 17:39:31 -040047const defaultWebRTCContext: IWebRTCContext = {
Charliec2c012f2022-10-05 14:09:28 -040048 localVideoRef: null,
49 remoteVideoRef: null,
simon9a8fe202022-11-15 18:25:49 -050050 mediaDevices: {
51 audioinput: [],
52 audiooutput: [],
53 videoinput: [],
54 },
Charlie461805e2022-11-09 10:40:15 -050055
56 contactId: '',
simonce2c0c42022-11-02 17:39:31 -040057
58 isAudioOn: false,
59 setAudioStatus: () => {},
60 isVideoOn: false,
61 setVideoStatus: () => {},
62
Charliec2c012f2022-10-05 14:09:28 -040063 sendWebRTCOffer: () => {},
Charliec2c012f2022-10-05 14:09:28 -040064};
65
simonce2c0c42022-11-02 17:39:31 -040066export const WebRTCContext = createContext<IWebRTCContext>(defaultWebRTCContext);
Charliec2c012f2022-10-05 14:09:28 -040067
simonce2c0c42022-11-02 17:39:31 -040068type WebRTCProviderProps = WithChildren & {
Charlie461805e2022-11-09 10:40:15 -050069 contactId: string;
simonce2c0c42022-11-02 17:39:31 -040070 isAudioOn?: boolean;
71 isVideoOn?: boolean;
72};
73
74// TODO: This is a WIP. The calling logic will be improved in other CRs
75export default ({
76 children,
77 isAudioOn: _isAudioOn = defaultWebRTCContext.isAudioOn,
78 isVideoOn: _isVideoOn = defaultWebRTCContext.isVideoOn,
Charlie461805e2022-11-09 10:40:15 -050079 contactId: _contactId = defaultWebRTCContext.contactId,
simonce2c0c42022-11-02 17:39:31 -040080}: WebRTCProviderProps) => {
81 const [isAudioOn, setIsAudioOn] = useState(_isAudioOn);
82 const [isVideoOn, setIsVideoOn] = useState(_isVideoOn);
Charliec2c012f2022-10-05 14:09:28 -040083 const localVideoRef = useRef<HTMLVideoElement>(null);
84 const remoteVideoRef = useRef<HTMLVideoElement>(null);
Charlie461805e2022-11-09 10:40:15 -050085 const { account } = useAuthContext();
86 const contactId = _contactId;
simonce2c0c42022-11-02 17:39:31 -040087 const [webRTCConnection, setWebRTCConnection] = useState<RTCPeerConnection | undefined>();
88 const localStreamRef = useRef<MediaStream>();
Issam E. Maghni0432cb72022-11-12 06:09:26 +000089 const webSocket = useContext(WebSocketContext);
simon9a8fe202022-11-15 18:25:49 -050090 const [mediaDevices, setMediaDevices] = useState<Record<MediaDeviceKind, MediaDeviceInfo[]>>(
91 defaultWebRTCContext.mediaDevices
92 );
93
94 useEffect(() => {
95 navigator.mediaDevices.enumerateDevices().then((devices) => {
96 const newMediaDevices: Record<MediaDeviceKind, MediaDeviceInfo[]> = {
97 audioinput: [],
98 audiooutput: [],
99 videoinput: [],
100 };
101
102 for (const device of devices) {
103 newMediaDevices[device.kind].push(device);
104 }
105
106 setMediaDevices(newMediaDevices);
107 });
108 }, []);
Charliec2c012f2022-10-05 14:09:28 -0400109
simonce2c0c42022-11-02 17:39:31 -0400110 useEffect(() => {
111 if (!webRTCConnection) {
112 // TODO use SFL iceServers
113 const iceConfig = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] };
114 setWebRTCConnection(new RTCPeerConnection(iceConfig));
115 }
116 }, [webRTCConnection]);
Charliec2c012f2022-10-05 14:09:28 -0400117
simonce2c0c42022-11-02 17:39:31 -0400118 useEffect(() => {
119 if (!webRTCConnection) {
120 return;
Charliec2c012f2022-10-05 14:09:28 -0400121 }
122
simonce2c0c42022-11-02 17:39:31 -0400123 if (isVideoOn || isAudioOn) {
124 try {
125 // TODO: When toggling mute on/off, the camera flickers
126 // https://git.jami.net/savoirfairelinux/jami-web/-/issues/90
127 navigator.mediaDevices
128 .getUserMedia({
129 audio: true,
130 video: true,
131 })
132 .then((stream) => {
133 if (localVideoRef.current) {
134 localVideoRef.current.srcObject = stream;
135 }
136
137 stream.getTracks().forEach((track) => {
138 if (track.kind === 'audio') {
139 track.enabled = isAudioOn;
140 } else if (track.kind === 'video') {
141 track.enabled = isVideoOn;
142 }
143 webRTCConnection.addTrack(track, stream);
144 });
145 localStreamRef.current = stream;
146 });
147 } catch (e) {
148 console.error('Could not get media devices: ', e);
Charliec2c012f2022-10-05 14:09:28 -0400149 }
simonce2c0c42022-11-02 17:39:31 -0400150 }
151
152 const icecandidateEventListener = (event: RTCPeerConnectionIceEvent) => {
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000153 if (event.candidate && webSocket) {
Charliec2c012f2022-10-05 14:09:28 -0400154 console.log('webRTCConnection : onicecandidate');
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000155 webSocket.send(WebSocketMessageType.IceCandidate, {
156 from: account.getId(),
157 to: contactId,
158 message: {
159 candidate: event.candidate,
Charlie461805e2022-11-09 10:40:15 -0500160 },
161 });
Charliec2c012f2022-10-05 14:09:28 -0400162 }
simonce2c0c42022-11-02 17:39:31 -0400163 };
164
165 const trackEventListener = (event: RTCTrackEvent) => {
166 console.log('remote TrackEvent');
Charliec2c012f2022-10-05 14:09:28 -0400167 if (remoteVideoRef.current) {
168 remoteVideoRef.current.srcObject = event.streams[0];
169 console.log('webRTCConnection : add remotetrack success');
170 }
simonce2c0c42022-11-02 17:39:31 -0400171 };
172
173 webRTCConnection.addEventListener('icecandidate', icecandidateEventListener);
174 webRTCConnection.addEventListener('track', trackEventListener);
175
176 return () => {
177 webRTCConnection.removeEventListener('icecandidate', icecandidateEventListener);
178 webRTCConnection.removeEventListener('track', trackEventListener);
179 };
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000180 }, [webRTCConnection, isVideoOn, isAudioOn, webSocket, contactId, account]);
simonce2c0c42022-11-02 17:39:31 -0400181
182 useEffect(() => {
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000183 if (!webRTCConnection || !webSocket) {
simonce2c0c42022-11-02 17:39:31 -0400184 return;
185 }
186
simona5c54ef2022-11-18 05:26:06 -0500187 const webRTCOfferListener = async (data: AccountTextMessage<WebRTCOfferMessage>) => {
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000188 if (webRTCConnection) {
189 await webRTCConnection.setRemoteDescription(new RTCSessionDescription(data.message.sdp));
Charlie461805e2022-11-09 10:40:15 -0500190 const mySdp = await webRTCConnection.createAnswer({
191 offerToReceiveAudio: true,
192 offerToReceiveVideo: true,
193 });
194 await webRTCConnection.setLocalDescription(new RTCSessionDescription(mySdp));
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000195 webSocket.send(WebSocketMessageType.WebRTCAnswer, {
196 from: account.getId(),
197 to: contactId,
198 message: {
199 sdp: mySdp,
Charlie461805e2022-11-09 10:40:15 -0500200 },
201 });
Charlie461805e2022-11-09 10:40:15 -0500202 }
simona5c54ef2022-11-18 05:26:06 -0500203 };
Charlie461805e2022-11-09 10:40:15 -0500204
simona5c54ef2022-11-18 05:26:06 -0500205 const webRTCAnswerListener = async (data: AccountTextMessage<WebRTCAnswerMessage>) => {
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000206 await webRTCConnection.setRemoteDescription(new RTCSessionDescription(data.message.sdp));
simonce2c0c42022-11-02 17:39:31 -0400207 console.log('get answer');
simona5c54ef2022-11-18 05:26:06 -0500208 };
Charlie461805e2022-11-09 10:40:15 -0500209
simona5c54ef2022-11-18 05:26:06 -0500210 const iceCandidateListener = async (data: AccountTextMessage<WebRTCIceCandidate>) => {
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000211 await webRTCConnection.addIceCandidate(new RTCIceCandidate(data.message.candidate));
Charlie461805e2022-11-09 10:40:15 -0500212 console.log('webRTCConnection : candidate add success');
simona5c54ef2022-11-18 05:26:06 -0500213 };
214
215 webSocket.bind(WebSocketMessageType.WebRTCOffer, webRTCOfferListener);
216 webSocket.bind(WebSocketMessageType.WebRTCAnswer, webRTCAnswerListener);
217 webSocket.bind(WebSocketMessageType.IceCandidate, iceCandidateListener);
218
219 return () => {
220 webSocket.unbind(WebSocketMessageType.WebRTCOffer, webRTCOfferListener);
221 webSocket.unbind(WebSocketMessageType.WebRTCAnswer, webRTCAnswerListener);
222 webSocket.unbind(WebSocketMessageType.IceCandidate, iceCandidateListener);
223 };
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000224 }, [account, contactId, webSocket, webRTCConnection]);
simonce2c0c42022-11-02 17:39:31 -0400225
226 const setAudioStatus = useCallback((isOn: boolean) => {
227 setIsAudioOn(isOn);
228 localStreamRef.current?.getAudioTracks().forEach((track) => {
229 track.enabled = isOn;
230 });
231 }, []);
232
233 const setVideoStatus = useCallback((isOn: boolean) => {
234 setIsVideoOn(isOn);
235 localStreamRef.current?.getVideoTracks().forEach((track) => {
236 track.enabled = isOn;
237 });
238 }, []);
Charliec2c012f2022-10-05 14:09:28 -0400239
240 const sendWebRTCOffer = useCallback(async () => {
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000241 if (webRTCConnection && webSocket) {
simon94fe53e2022-11-10 12:51:58 -0500242 const sdp = await webRTCConnection.createOffer({
243 offerToReceiveAudio: true,
244 offerToReceiveVideo: true,
245 });
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000246 webSocket.send(WebSocketMessageType.WebRTCOffer, {
247 from: account.getId(),
248 to: contactId,
249 message: {
250 sdp,
simon94fe53e2022-11-10 12:51:58 -0500251 },
252 });
253 await webRTCConnection.setLocalDescription(new RTCSessionDescription(sdp));
Charliec2c012f2022-10-05 14:09:28 -0400254 }
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000255 }, [account, contactId, webSocket, webRTCConnection]);
Charliec2c012f2022-10-05 14:09:28 -0400256
257 return (
258 <WebRTCContext.Provider
259 value={{
260 localVideoRef,
261 remoteVideoRef,
simon9a8fe202022-11-15 18:25:49 -0500262 mediaDevices,
Charlie461805e2022-11-09 10:40:15 -0500263 contactId,
simonce2c0c42022-11-02 17:39:31 -0400264 isAudioOn,
265 setAudioStatus,
266 isVideoOn,
267 setVideoStatus,
268 sendWebRTCOffer,
Charliec2c012f2022-10-05 14:09:28 -0400269 }}
270 >
271 {children}
272 </WebRTCContext.Provider>
273 );
274};