import React, { useEffect, useContext, useState, useRef, useMemo, useCallback } from 'react'; import Peer from 'simple-peer'; import useAuth from './AuthProvider'; import { useImmer } from 'use-immer'; import useSockets from './SocketProvider'; import { RtcAnswerSomeoneRequest, RtcCallRequest, RtcNewUserJoinedResponse, RtcReceiveAnswerResponse, ServerRequestChannels, ServerResponseChannels, } from '@nirvana/core/sockets/channels'; import toast from 'react-hot-toast'; import useTerminalProvider from './TerminalProvider'; import MasterLineData from '@nirvana/core/models/masterLineData.model'; import { useEffectOnce } from 'react-use'; const iceServers = [ { urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun1.l.google.com:19302' }, { urls: 'stun:stun2.l.google.com:19302' }, { urls: 'stun:stun3.l.google.com:19302' }, { urls: 'stun:stun4.l.google.com:19302' }, { url: 'turn:numb.viagenie.ca', credential: 'muazkh', username: 'webrtc@live.com', }, { url: 'turn:numb.viagenie.ca', credential: 'muazkh', username: 'webrtc@live.com', }, { url: 'turn:192.158.29.39:3478?transport=udp', credential: 'JZEOEt2V3Qb0y27GRntt2u2PAYA=', username: '28224511:1379330808', }, { url: 'turn:192.158.29.39:3478?transport=tcp', credential: 'JZEOEt2V3Qb0y27GRntt2u2PAYA=', username: '28224511:1379330808', }, ]; type LinePeerMap = { [lineId: string]: { userId: string; peer: Peer; myMediaStream?: MediaStream; peerMediaStream?: MediaStream; }[]; }; interface IStreamProvider { peerMap: LinePeerMap; userLocalStream?: MediaStream; } const StreamProviderContext = React.createContext({ peerMap: {}, }); export function StreamProvider({ children }: { children: React.ReactChild }) { const { roomsMap } = useTerminalProvider(); const { user } = useAuth(); const { $ws } = useSockets(); const [peerMap, updatePeerMap] = useImmer({}); const [userLocalStream, setUserLocalStream] = useState(); const [localUserStreams, setLocalUserStreams] = useImmer<{ [lineId: string]: MediaStream }>({}); useEffect(() => { $ws.on(ServerResponseChannels.RTC_NEW_USER_JOINED, (res: RtcNewUserJoinedResponse) => { toast.success('NEWBIE JOINED!!!'); console.log('someone calling me', res); const peerForMeAndNewbie = new Peer({ initiator: false, trickle: false, // prevents the multiple tries on different ice servers and signal from getting called a bunch of times stream: userLocalStream, config: { iceServers, }, }); peerForMeAndNewbie.signal(res.simplePeerSignal); handleAddPeer(res.lineId, res.userWhoCalled, peerForMeAndNewbie, userLocalStream); peerForMeAndNewbie.on('signal', (signal) => { console.log('sending an answer to the slave', res); $ws.emit( ServerRequestChannels.RTC_ANSWER_SOMEONE_FOR_LINE, new RtcAnswerSomeoneRequest(res.userWhoCalled, res.lineId, signal), ); }); peerForMeAndNewbie.on('stream', (remoteStream: MediaStream) => { handleGotPeerRemoteStream(res.lineId, res.userWhoCalled, remoteStream); }); // !adding stream here causes race condition of the signal event just running over and over again // see if I have a stream for this channel or line // create one if not, and add to stream // navigator.mediaDevices // .getUserMedia({ video: true, audio: true }) // .then((currStream: MediaStream) => { // peerForMeAndNewbie.addStream(currStream); // }); }); $ws.on(ServerResponseChannels.RTC_RECEIVING_MASTER_ANSWER, (res: RtcReceiveAnswerResponse) => { toast.success('MASTER gave me an answer!!!'); console.log('master gave me this answer: ', res); // find this person in peer map updatePeerMap((draft) => { const localPeerForMasterAndMe = draft[res.lineId].find( (currPeerRelationship) => currPeerRelationship.userId === res.masterUserId, ); if (localPeerForMasterAndMe) localPeerForMasterAndMe.peer.signal(res.simplePeerSignal); }); }); return () => { $ws.removeAllListeners(ServerResponseChannels.RTC_NEW_USER_JOINED); $ws.removeAllListeners(ServerResponseChannels.RTC_RECEIVING_MASTER_ANSWER); }; }, [updatePeerMap, $ws, userLocalStream]); useEffect(() => { navigator.mediaDevices.enumerateDevices().then((devices) => { const uniqueDevices = []; const uniqueGroupIds = []; devices.forEach((device) => { if (!uniqueGroupIds.includes(device.groupId)) { uniqueDevices.push(device); uniqueGroupIds.push(device.groupId); } }); console.log(uniqueDevices); }); navigator.mediaDevices .getUserMedia({ video: true, audio: true }) .then((localMediaStream: MediaStream) => { setUserLocalStream(localMediaStream); }); }, []); console.log(`peer map: `, peerMap); const handleAddPeer = useCallback( (lineId: string, userId: string, peerObj: Peer, myMediaStream?: MediaStream) => { updatePeerMap((draft) => { // for trickling, if we already have a peer for this line and user, then just replace const existingUserLinePeerRelation = draft[lineId]?.find( (currPeerRelation) => currPeerRelation.userId === userId, ); if (existingUserLinePeerRelation) { return draft; } if (draft[lineId]) { draft[lineId].push({ userId, peer: peerObj, myMediaStream }); } else { draft[lineId] = [{ userId, peer: peerObj, myMediaStream }]; } }); }, [updatePeerMap], ); const handleGotPeerRemoteStream = useCallback( (lineId: string, userId: string, remoteStream: MediaStream) => { updatePeerMap((draft) => { // for trickling, if we already have a peer for this line and user, then just replace const existingUserLinePeerRelation = draft[lineId]?.find( (currPeerRelation) => currPeerRelation.userId === userId, ); if (existingUserLinePeerRelation) { existingUserLinePeerRelation.peerMediaStream = remoteStream; } }); }, [updatePeerMap], ); return ( {/* handles stream connections */} {Object.values(roomsMap).map((line) => { if (line.tunedInMemberIds?.includes(user._id.toString())) return ( currMemberId !== user._id.toString(), )} handleGotPeerRemoteStream={handleGotPeerRemoteStream} /> ); })} {children} ); } export default function useStreams() { return useContext(StreamProviderContext); } const MemoLineConnector = React.memo(LineConnector); // handle managing stream connections for one line function LineConnector({ lineId, membersToCall, handleAddPeer, handleGotPeerRemoteStream, }: { lineId: string; membersToCall: string[]; handleAddPeer: ( lineId: string, userId: string, peerObj: Peer, myMediaStream?: MediaStream, ) => void; handleGotPeerRemoteStream: (lineId: string, userId: string, remoteStream: MediaStream) => void; }) { const { $ws } = useSockets(); console.log('rendering this piece of shit'); useEffectOnce(() => { console.log('got initial list for this channel that I am tuned into'); console.log(membersToCall); // call these people // tell them what line I'm calling about, so that they can use the signal for the right peer object // also so that I can signal for the peer object relationship between me and this other person for this particular channel // then add in the stream to this local peer relationship object // todo get the user media selections navigator.mediaDevices .getUserMedia({ video: true, audio: true }) .then((localMediaStream: MediaStream) => { // todo check if already in peer map? keeping it simple for now // a peer relationship between me and someone for this particular channel so that I can just enable or disable this particular stream // object instead of managing different ones // bandwidth wise, would be uploading stream to one room at a time but downloading a x b streams but someone can't // stream in two at same time anyway toast.success('CALLING bunch of people!!!'); console.log('Calling these folks', membersToCall); console.log('for line', lineId); // todo...call in parallel membersToCall.map((memberId) => { const connectingToast = toast.loading('calling peer for a snappy experience'); const localPeerConnection = new Peer({ initiator: true, stream: localMediaStream, trickle: false, // prevents the multiple tries on different ice servers and signal from getting called a bunch of times, config: { iceServers, }, }); localPeerConnection.on('signal', (signal) => { console.log('have a signal to make call to someone '); $ws.emit( ServerRequestChannels.RTC_CALL_SOMEONE_FOR_LINE, new RtcCallRequest(memberId, lineId, signal), ); toast.dismiss(connectingToast); // sending back the connection to the parent // so that we can accept the answer later on handleAddPeer(lineId, memberId, localPeerConnection, localMediaStream); }); localPeerConnection.on('stream', (remoteStream: MediaStream) => { handleGotPeerRemoteStream(lineId, memberId, remoteStream); }); }); }); }); return <>; }