blob: 7dc1611caa96aeef261faa2f417684bb4b931332 [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
Issam E. Maghni0432cb72022-11-12 06:09:26 +000019import { WebSocketMessageType } from 'jami-web-common';
Charlie461805e2022-11-09 10:40:15 -050020import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';
Charliec2c012f2022-10-05 14:09:28 -040021
22import { WithChildren } from '../utils/utils';
Charlie461805e2022-11-09 10:40:15 -050023import { useAuthContext } from './AuthProvider';
24import { WebSocketContext } from './WebSocketProvider';
Charliec2c012f2022-10-05 14:09:28 -040025
26interface IWebRTCContext {
27 localVideoRef: React.RefObject<HTMLVideoElement> | null;
28 remoteVideoRef: React.RefObject<HTMLVideoElement> | null;
Charlie461805e2022-11-09 10:40:15 -050029
simon9a8fe202022-11-15 18:25:49 -050030 mediaDevices: Record<MediaDeviceKind, MediaDeviceInfo[]>;
31
Charlie461805e2022-11-09 10:40:15 -050032 contactId: string;
simonce2c0c42022-11-02 17:39:31 -040033
34 isAudioOn: boolean;
35 setAudioStatus: (isOn: boolean) => void;
36 isVideoOn: boolean;
37 setVideoStatus: (isOn: boolean) => void;
38 sendWebRTCOffer: () => void;
Charliec2c012f2022-10-05 14:09:28 -040039}
40
simonce2c0c42022-11-02 17:39:31 -040041const defaultWebRTCContext: IWebRTCContext = {
Charliec2c012f2022-10-05 14:09:28 -040042 localVideoRef: null,
43 remoteVideoRef: null,
simon9a8fe202022-11-15 18:25:49 -050044 mediaDevices: {
45 audioinput: [],
46 audiooutput: [],
47 videoinput: [],
48 },
Charlie461805e2022-11-09 10:40:15 -050049
50 contactId: '',
simonce2c0c42022-11-02 17:39:31 -040051
52 isAudioOn: false,
53 setAudioStatus: () => {},
54 isVideoOn: false,
55 setVideoStatus: () => {},
56
Charliec2c012f2022-10-05 14:09:28 -040057 sendWebRTCOffer: () => {},
Charliec2c012f2022-10-05 14:09:28 -040058};
59
simonce2c0c42022-11-02 17:39:31 -040060export const WebRTCContext = createContext<IWebRTCContext>(defaultWebRTCContext);
Charliec2c012f2022-10-05 14:09:28 -040061
simonce2c0c42022-11-02 17:39:31 -040062type WebRTCProviderProps = WithChildren & {
Charlie461805e2022-11-09 10:40:15 -050063 contactId: string;
simonce2c0c42022-11-02 17:39:31 -040064 isAudioOn?: boolean;
65 isVideoOn?: boolean;
66};
67
68// TODO: This is a WIP. The calling logic will be improved in other CRs
69export default ({
70 children,
71 isAudioOn: _isAudioOn = defaultWebRTCContext.isAudioOn,
72 isVideoOn: _isVideoOn = defaultWebRTCContext.isVideoOn,
Charlie461805e2022-11-09 10:40:15 -050073 contactId: _contactId = defaultWebRTCContext.contactId,
simonce2c0c42022-11-02 17:39:31 -040074}: WebRTCProviderProps) => {
75 const [isAudioOn, setIsAudioOn] = useState(_isAudioOn);
76 const [isVideoOn, setIsVideoOn] = useState(_isVideoOn);
Charliec2c012f2022-10-05 14:09:28 -040077 const localVideoRef = useRef<HTMLVideoElement>(null);
78 const remoteVideoRef = useRef<HTMLVideoElement>(null);
Charlie461805e2022-11-09 10:40:15 -050079 const { account } = useAuthContext();
80 const contactId = _contactId;
simonce2c0c42022-11-02 17:39:31 -040081 const [webRTCConnection, setWebRTCConnection] = useState<RTCPeerConnection | undefined>();
82 const localStreamRef = useRef<MediaStream>();
Issam E. Maghni0432cb72022-11-12 06:09:26 +000083 const webSocket = useContext(WebSocketContext);
simon9a8fe202022-11-15 18:25:49 -050084 const [mediaDevices, setMediaDevices] = useState<Record<MediaDeviceKind, MediaDeviceInfo[]>>(
85 defaultWebRTCContext.mediaDevices
86 );
87
88 useEffect(() => {
89 navigator.mediaDevices.enumerateDevices().then((devices) => {
90 const newMediaDevices: Record<MediaDeviceKind, MediaDeviceInfo[]> = {
91 audioinput: [],
92 audiooutput: [],
93 videoinput: [],
94 };
95
96 for (const device of devices) {
97 newMediaDevices[device.kind].push(device);
98 }
99
100 setMediaDevices(newMediaDevices);
101 });
102 }, []);
Charliec2c012f2022-10-05 14:09:28 -0400103
simonce2c0c42022-11-02 17:39:31 -0400104 useEffect(() => {
105 if (!webRTCConnection) {
106 // TODO use SFL iceServers
107 const iceConfig = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] };
108 setWebRTCConnection(new RTCPeerConnection(iceConfig));
109 }
110 }, [webRTCConnection]);
Charliec2c012f2022-10-05 14:09:28 -0400111
simonce2c0c42022-11-02 17:39:31 -0400112 useEffect(() => {
113 if (!webRTCConnection) {
114 return;
Charliec2c012f2022-10-05 14:09:28 -0400115 }
116
simonce2c0c42022-11-02 17:39:31 -0400117 if (isVideoOn || isAudioOn) {
118 try {
119 // TODO: When toggling mute on/off, the camera flickers
120 // https://git.jami.net/savoirfairelinux/jami-web/-/issues/90
121 navigator.mediaDevices
122 .getUserMedia({
123 audio: true,
124 video: true,
125 })
126 .then((stream) => {
127 if (localVideoRef.current) {
128 localVideoRef.current.srcObject = stream;
129 }
130
131 stream.getTracks().forEach((track) => {
132 if (track.kind === 'audio') {
133 track.enabled = isAudioOn;
134 } else if (track.kind === 'video') {
135 track.enabled = isVideoOn;
136 }
137 webRTCConnection.addTrack(track, stream);
138 });
139 localStreamRef.current = stream;
140 });
141 } catch (e) {
142 console.error('Could not get media devices: ', e);
Charliec2c012f2022-10-05 14:09:28 -0400143 }
simonce2c0c42022-11-02 17:39:31 -0400144 }
145
146 const icecandidateEventListener = (event: RTCPeerConnectionIceEvent) => {
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000147 if (event.candidate && webSocket) {
Charliec2c012f2022-10-05 14:09:28 -0400148 console.log('webRTCConnection : onicecandidate');
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000149 webSocket.send(WebSocketMessageType.IceCandidate, {
150 from: account.getId(),
151 to: contactId,
152 message: {
153 candidate: event.candidate,
Charlie461805e2022-11-09 10:40:15 -0500154 },
155 });
Charliec2c012f2022-10-05 14:09:28 -0400156 }
simonce2c0c42022-11-02 17:39:31 -0400157 };
158
159 const trackEventListener = (event: RTCTrackEvent) => {
160 console.log('remote TrackEvent');
Charliec2c012f2022-10-05 14:09:28 -0400161 if (remoteVideoRef.current) {
162 remoteVideoRef.current.srcObject = event.streams[0];
163 console.log('webRTCConnection : add remotetrack success');
164 }
simonce2c0c42022-11-02 17:39:31 -0400165 };
166
167 webRTCConnection.addEventListener('icecandidate', icecandidateEventListener);
168 webRTCConnection.addEventListener('track', trackEventListener);
169
170 return () => {
171 webRTCConnection.removeEventListener('icecandidate', icecandidateEventListener);
172 webRTCConnection.removeEventListener('track', trackEventListener);
173 };
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000174 }, [webRTCConnection, isVideoOn, isAudioOn, webSocket, contactId, account]);
simonce2c0c42022-11-02 17:39:31 -0400175
176 useEffect(() => {
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000177 if (!webRTCConnection || !webSocket) {
simonce2c0c42022-11-02 17:39:31 -0400178 return;
179 }
180
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000181 webSocket.bind(WebSocketMessageType.WebRTCOffer, async (data) => {
182 if (webRTCConnection) {
183 await webRTCConnection.setRemoteDescription(new RTCSessionDescription(data.message.sdp));
Charlie461805e2022-11-09 10:40:15 -0500184 const mySdp = await webRTCConnection.createAnswer({
185 offerToReceiveAudio: true,
186 offerToReceiveVideo: true,
187 });
188 await webRTCConnection.setLocalDescription(new RTCSessionDescription(mySdp));
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000189 webSocket.send(WebSocketMessageType.WebRTCAnswer, {
190 from: account.getId(),
191 to: contactId,
192 message: {
193 sdp: mySdp,
Charlie461805e2022-11-09 10:40:15 -0500194 },
195 });
Charlie461805e2022-11-09 10:40:15 -0500196 }
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000197 });
Charlie461805e2022-11-09 10:40:15 -0500198
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000199 webSocket.bind(WebSocketMessageType.WebRTCAnswer, async (data) => {
200 await webRTCConnection.setRemoteDescription(new RTCSessionDescription(data.message.sdp));
simonce2c0c42022-11-02 17:39:31 -0400201 console.log('get answer');
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000202 });
Charlie461805e2022-11-09 10:40:15 -0500203
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000204 webSocket.bind(WebSocketMessageType.IceCandidate, async (data) => {
205 await webRTCConnection.addIceCandidate(new RTCIceCandidate(data.message.candidate));
Charlie461805e2022-11-09 10:40:15 -0500206 console.log('webRTCConnection : candidate add success');
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000207 });
208 }, [account, contactId, webSocket, webRTCConnection]);
simonce2c0c42022-11-02 17:39:31 -0400209
210 const setAudioStatus = useCallback((isOn: boolean) => {
211 setIsAudioOn(isOn);
212 localStreamRef.current?.getAudioTracks().forEach((track) => {
213 track.enabled = isOn;
214 });
215 }, []);
216
217 const setVideoStatus = useCallback((isOn: boolean) => {
218 setIsVideoOn(isOn);
219 localStreamRef.current?.getVideoTracks().forEach((track) => {
220 track.enabled = isOn;
221 });
222 }, []);
Charliec2c012f2022-10-05 14:09:28 -0400223
224 const sendWebRTCOffer = useCallback(async () => {
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000225 if (webRTCConnection && webSocket) {
simon94fe53e2022-11-10 12:51:58 -0500226 const sdp = await webRTCConnection.createOffer({
227 offerToReceiveAudio: true,
228 offerToReceiveVideo: true,
229 });
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000230 webSocket.send(WebSocketMessageType.WebRTCOffer, {
231 from: account.getId(),
232 to: contactId,
233 message: {
234 sdp,
simon94fe53e2022-11-10 12:51:58 -0500235 },
236 });
237 await webRTCConnection.setLocalDescription(new RTCSessionDescription(sdp));
Charliec2c012f2022-10-05 14:09:28 -0400238 }
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000239 }, [account, contactId, webSocket, webRTCConnection]);
Charliec2c012f2022-10-05 14:09:28 -0400240
241 return (
242 <WebRTCContext.Provider
243 value={{
244 localVideoRef,
245 remoteVideoRef,
simon9a8fe202022-11-15 18:25:49 -0500246 mediaDevices,
Charlie461805e2022-11-09 10:40:15 -0500247 contactId,
simonce2c0c42022-11-02 17:39:31 -0400248 isAudioOn,
249 setAudioStatus,
250 isVideoOn,
251 setVideoStatus,
252 sendWebRTCOffer,
Charliec2c012f2022-10-05 14:09:28 -0400253 }}
254 >
255 {children}
256 </WebRTCContext.Provider>
257 );
258};