311 lines
10 KiB
TypeScript
311 lines
10 KiB
TypeScript
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: '[email protected]',
|
|
},
|
|
{
|
|
url: 'turn:numb.viagenie.ca',
|
|
credential: 'muazkh',
|
|
username: '[email protected]',
|
|
},
|
|
{
|
|
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<IStreamProvider>({
|
|
peerMap: {},
|
|
});
|
|
|
|
export function StreamProvider({ children }: { children: React.ReactChild }) {
|
|
const { roomsMap } = useTerminalProvider();
|
|
const { user } = useAuth();
|
|
|
|
const { $ws } = useSockets();
|
|
|
|
const [peerMap, updatePeerMap] = useImmer<LinePeerMap>({});
|
|
|
|
const [userLocalStream, setUserLocalStream] = useState<MediaStream>();
|
|
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 (
|
|
<StreamProviderContext.Provider value={{ peerMap, userLocalStream }}>
|
|
{/* handles stream connections */}
|
|
{Object.values(roomsMap).map((line) => {
|
|
if (line.tunedInMemberIds?.includes(user._id.toString()))
|
|
return (
|
|
<MemoLineConnector
|
|
key={`streamConnector-${line.lineDetails._id.toString()}`}
|
|
lineId={line.lineDetails._id.toString()}
|
|
handleAddPeer={handleAddPeer}
|
|
membersToCall={line.tunedInMemberIds.filter(
|
|
(currMemberId) => currMemberId !== user._id.toString(),
|
|
)}
|
|
handleGotPeerRemoteStream={handleGotPeerRemoteStream}
|
|
/>
|
|
);
|
|
})}
|
|
|
|
{children}
|
|
</StreamProviderContext.Provider>
|
|
);
|
|
}
|
|
|
|
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 <></>;
|
|
}
|