blob: be5a045dbe438da0f46253f28184d0bdf2b33c14 [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
Charlie461805e2022-11-09 10:40:15 -050019import { WebRTCIceCandidate, WebRTCSDP, WebSocketMessage, WebSocketMessageType } from 'jami-web-common';
20import 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
30 contactId: string;
simonce2c0c42022-11-02 17:39:31 -040031
32 isAudioOn: boolean;
33 setAudioStatus: (isOn: boolean) => void;
34 isVideoOn: boolean;
35 setVideoStatus: (isOn: boolean) => void;
36 sendWebRTCOffer: () => void;
Charliec2c012f2022-10-05 14:09:28 -040037}
38
simonce2c0c42022-11-02 17:39:31 -040039const defaultWebRTCContext: IWebRTCContext = {
Charliec2c012f2022-10-05 14:09:28 -040040 localVideoRef: null,
41 remoteVideoRef: null,
Charlie461805e2022-11-09 10:40:15 -050042
43 contactId: '',
simonce2c0c42022-11-02 17:39:31 -040044
45 isAudioOn: false,
46 setAudioStatus: () => {},
47 isVideoOn: false,
48 setVideoStatus: () => {},
49
Charliec2c012f2022-10-05 14:09:28 -040050 sendWebRTCOffer: () => {},
Charliec2c012f2022-10-05 14:09:28 -040051};
52
simonce2c0c42022-11-02 17:39:31 -040053export const WebRTCContext = createContext<IWebRTCContext>(defaultWebRTCContext);
Charliec2c012f2022-10-05 14:09:28 -040054
simonce2c0c42022-11-02 17:39:31 -040055type WebRTCProviderProps = WithChildren & {
Charlie461805e2022-11-09 10:40:15 -050056 contactId: string;
simonce2c0c42022-11-02 17:39:31 -040057 isAudioOn?: boolean;
58 isVideoOn?: boolean;
59};
60
61// TODO: This is a WIP. The calling logic will be improved in other CRs
62export default ({
63 children,
64 isAudioOn: _isAudioOn = defaultWebRTCContext.isAudioOn,
65 isVideoOn: _isVideoOn = defaultWebRTCContext.isVideoOn,
Charlie461805e2022-11-09 10:40:15 -050066 contactId: _contactId = defaultWebRTCContext.contactId,
simonce2c0c42022-11-02 17:39:31 -040067}: WebRTCProviderProps) => {
68 const [isAudioOn, setIsAudioOn] = useState(_isAudioOn);
69 const [isVideoOn, setIsVideoOn] = useState(_isVideoOn);
Charliec2c012f2022-10-05 14:09:28 -040070 const localVideoRef = useRef<HTMLVideoElement>(null);
71 const remoteVideoRef = useRef<HTMLVideoElement>(null);
Charlie461805e2022-11-09 10:40:15 -050072 const { account } = useAuthContext();
73 const contactId = _contactId;
simonce2c0c42022-11-02 17:39:31 -040074 const [webRTCConnection, setWebRTCConnection] = useState<RTCPeerConnection | undefined>();
75 const localStreamRef = useRef<MediaStream>();
Charlie461805e2022-11-09 10:40:15 -050076 const socket = useContext(WebSocketContext);
Charliec2c012f2022-10-05 14:09:28 -040077
simonce2c0c42022-11-02 17:39:31 -040078 useEffect(() => {
79 if (!webRTCConnection) {
80 // TODO use SFL iceServers
81 const iceConfig = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] };
82 setWebRTCConnection(new RTCPeerConnection(iceConfig));
83 }
84 }, [webRTCConnection]);
Charliec2c012f2022-10-05 14:09:28 -040085
simonce2c0c42022-11-02 17:39:31 -040086 useEffect(() => {
87 if (!webRTCConnection) {
88 return;
Charliec2c012f2022-10-05 14:09:28 -040089 }
90
simonce2c0c42022-11-02 17:39:31 -040091 if (isVideoOn || isAudioOn) {
92 try {
93 // TODO: When toggling mute on/off, the camera flickers
94 // https://git.jami.net/savoirfairelinux/jami-web/-/issues/90
95 navigator.mediaDevices
96 .getUserMedia({
97 audio: true,
98 video: true,
99 })
100 .then((stream) => {
101 if (localVideoRef.current) {
102 localVideoRef.current.srcObject = stream;
103 }
104
105 stream.getTracks().forEach((track) => {
106 if (track.kind === 'audio') {
107 track.enabled = isAudioOn;
108 } else if (track.kind === 'video') {
109 track.enabled = isVideoOn;
110 }
111 webRTCConnection.addTrack(track, stream);
112 });
113 localStreamRef.current = stream;
114 });
115 } catch (e) {
116 console.error('Could not get media devices: ', e);
Charliec2c012f2022-10-05 14:09:28 -0400117 }
simonce2c0c42022-11-02 17:39:31 -0400118 }
119
120 const icecandidateEventListener = (event: RTCPeerConnectionIceEvent) => {
Charlie461805e2022-11-09 10:40:15 -0500121 if (event.candidate && socket) {
Charliec2c012f2022-10-05 14:09:28 -0400122 console.log('webRTCConnection : onicecandidate');
Charlie461805e2022-11-09 10:40:15 -0500123 socket.send({
124 type: WebSocketMessageType.IceCandidate,
125 data: {
126 from: account.getId(),
127 to: contactId,
128 message: {
129 candidate: event.candidate,
130 },
131 },
132 });
Charliec2c012f2022-10-05 14:09:28 -0400133 }
simonce2c0c42022-11-02 17:39:31 -0400134 };
135
136 const trackEventListener = (event: RTCTrackEvent) => {
137 console.log('remote TrackEvent');
Charliec2c012f2022-10-05 14:09:28 -0400138 if (remoteVideoRef.current) {
139 remoteVideoRef.current.srcObject = event.streams[0];
140 console.log('webRTCConnection : add remotetrack success');
141 }
simonce2c0c42022-11-02 17:39:31 -0400142 };
143
144 webRTCConnection.addEventListener('icecandidate', icecandidateEventListener);
145 webRTCConnection.addEventListener('track', trackEventListener);
146
147 return () => {
148 webRTCConnection.removeEventListener('icecandidate', icecandidateEventListener);
149 webRTCConnection.removeEventListener('track', trackEventListener);
150 };
Charlie461805e2022-11-09 10:40:15 -0500151 }, [webRTCConnection, isVideoOn, isAudioOn, socket, contactId, account]);
simonce2c0c42022-11-02 17:39:31 -0400152
153 useEffect(() => {
Charlie461805e2022-11-09 10:40:15 -0500154 if (!webRTCConnection || !socket) {
simonce2c0c42022-11-02 17:39:31 -0400155 return;
156 }
157
Charlie461805e2022-11-09 10:40:15 -0500158 const sendWebRTCAnswer = async (message: WebSocketMessage) => {
159 if (webRTCConnection && socket) {
160 const remoteSdp: RTCSessionDescriptionInit = (message.data.message as WebRTCSDP).sdp;
161 await webRTCConnection.setRemoteDescription(new RTCSessionDescription(remoteSdp));
162 const mySdp = await webRTCConnection.createAnswer({
163 offerToReceiveAudio: true,
164 offerToReceiveVideo: true,
165 });
166 await webRTCConnection.setLocalDescription(new RTCSessionDescription(mySdp));
167 socket.send({
168 type: WebSocketMessageType.WebRTCAnswer,
169 data: {
170 from: account.getId(),
171 to: contactId,
172 message: {
173 sdp: mySdp,
174 },
175 },
176 });
177 console.log('get offer and aswering');
178 }
179 };
180
181 const handleWebRTCAnswer = async (message: WebSocketMessage) => {
182 const remoteSdp: RTCSessionDescriptionInit = (message.data.message as WebRTCSDP).sdp;
simonce2c0c42022-11-02 17:39:31 -0400183 await webRTCConnection.setRemoteDescription(new RTCSessionDescription(remoteSdp));
simonce2c0c42022-11-02 17:39:31 -0400184 console.log('get answer');
simonce2c0c42022-11-02 17:39:31 -0400185 };
Charlie461805e2022-11-09 10:40:15 -0500186
187 const addIceCandidate = async (message: WebSocketMessage) => {
188 const candidate: RTCIceCandidateInit = (message.data.message as WebRTCIceCandidate).candidate;
189 await webRTCConnection.addIceCandidate(new RTCIceCandidate(candidate));
190 console.log('webRTCConnection : candidate add success');
191 };
192
193 socket.bind(WebSocketMessageType.WebRTCOffer, sendWebRTCAnswer);
194 socket.bind(WebSocketMessageType.WebRTCAnswer, handleWebRTCAnswer);
195 socket.bind(WebSocketMessageType.IceCandidate, addIceCandidate);
196 }, [account, contactId, socket, webRTCConnection]);
simonce2c0c42022-11-02 17:39:31 -0400197
198 const setAudioStatus = useCallback((isOn: boolean) => {
199 setIsAudioOn(isOn);
200 localStreamRef.current?.getAudioTracks().forEach((track) => {
201 track.enabled = isOn;
202 });
203 }, []);
204
205 const setVideoStatus = useCallback((isOn: boolean) => {
206 setIsVideoOn(isOn);
207 localStreamRef.current?.getVideoTracks().forEach((track) => {
208 track.enabled = isOn;
209 });
210 }, []);
Charliec2c012f2022-10-05 14:09:28 -0400211
212 const sendWebRTCOffer = useCallback(async () => {
Charlie461805e2022-11-09 10:40:15 -0500213 if (webRTCConnection && socket) {
simonce2c0c42022-11-02 17:39:31 -0400214 webRTCConnection
215 .createOffer({
Charliec2c012f2022-10-05 14:09:28 -0400216 offerToReceiveAudio: true,
217 offerToReceiveVideo: true,
simonce2c0c42022-11-02 17:39:31 -0400218 })
219 .then((sdp) => {
Charlie461805e2022-11-09 10:40:15 -0500220 socket.send({
221 type: WebSocketMessageType.WebRTCOffer,
222 data: {
223 from: account.getId(),
224 to: contactId,
225 message: {
226 sdp: sdp,
227 },
228 },
229 });
simonce2c0c42022-11-02 17:39:31 -0400230 webRTCConnection.setLocalDescription(new RTCSessionDescription(sdp));
Charliec2c012f2022-10-05 14:09:28 -0400231 });
Charliec2c012f2022-10-05 14:09:28 -0400232 }
Charlie461805e2022-11-09 10:40:15 -0500233 }, [account, contactId, socket, webRTCConnection]);
Charliec2c012f2022-10-05 14:09:28 -0400234
235 return (
236 <WebRTCContext.Provider
237 value={{
238 localVideoRef,
239 remoteVideoRef,
Charlie461805e2022-11-09 10:40:15 -0500240 contactId,
simonce2c0c42022-11-02 17:39:31 -0400241 isAudioOn,
242 setAudioStatus,
243 isVideoOn,
244 setVideoStatus,
245 sendWebRTCOffer,
Charliec2c012f2022-10-05 14:09:28 -0400246 }}
247 >
248 {children}
249 </WebRTCContext.Provider>
250 );
251};