Files
nirvana-v3/packages/desktop/src/providers/StreamProvider.tsx
T

367 lines
12 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 videoConstraints = {
frameRate: 30,
width: { max: 100 },
height: { max: 200 },
};
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' },
// { urls: 'stun:global.stun.twilio.com:3478?transport=udp' },
// {
// 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:turn.bistri.com:80',
// credential: 'homeo',
// username: 'homeo',
// },
// {
// url: 'turn:turn.anyfirewall.com:443?transport=tcp',
// credential: 'webrtc',
// username: 'webrtc',
// },
{
url: 'turn:openrelay.metered.ca:80',
credential: 'openrelayproject',
username: 'openrelayproject',
},
];
type LineStreamData = {
localStreamForLine?: MediaStream;
peerRelations: {
userId: string;
peer: Peer;
peerMediaStream?: MediaStream;
}[];
};
type LinePeerMap = {
[lineId: string]: LineStreamData;
};
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 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]?.peerRelations?.find(
(currPeerRelation) => currPeerRelation.userId === userId,
);
if (existingUserLinePeerRelation) {
existingUserLinePeerRelation.peerMediaStream = remoteStream;
}
});
},
[updatePeerMap],
);
const setLocalStreamForLine = useCallback(
(lineId: string, localStreamForLine: MediaStream) => {
updatePeerMap((draft) => {
draft[lineId] = { ...draft[lineId], localStreamForLine };
});
},
[updatePeerMap],
);
const handleAddPeer = useCallback(
(lineId: string, userId: string, peerObj: Peer) => {
updatePeerMap((draft) => {
// for trickling, if we already have a peer for this line and user, then just replace
const existingUserLinePeerRelation = draft[lineId]?.peerRelations?.find(
(currPeerRelation) => currPeerRelation.userId === userId,
);
if (existingUserLinePeerRelation) {
return draft;
}
if (draft[lineId]?.peerRelations) {
draft[lineId].peerRelations.push({ userId, peer: peerObj });
} else {
draft[lineId] = { ...draft[lineId], peerRelations: [{ userId, peer: peerObj }] };
}
});
},
[updatePeerMap],
);
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: true, // prevents the multiple tries on different ice servers and signal from getting called a bunch of times
stream: peerMap[res.lineId]?.localStreamForLine,
config: {
iceServers,
},
});
peerForMeAndNewbie.signal(res.simplePeerSignal);
handleAddPeer(res.lineId, res.userWhoCalled, peerForMeAndNewbie);
// make sure this peer gets destroyed to remove this listener
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);
});
});
$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]?.peerRelations?.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, peerMap, handleGotPeerRemoteStream, handleAddPeer]);
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: videoConstraints,
audio: true,
})
.then((localMediaStream: MediaStream) => {
setUserLocalStream(localMediaStream);
});
}, []);
console.log(`peer map: `, peerMap);
// manage untuning including myself
useEffect(() => {
const tunedUsersForLines: { [lineId: string]: string[] } = {};
Object.values(roomsMap).map((currentLine) => {
if (currentLine.tunedInMemberIds)
tunedUsersForLines[currentLine.lineDetails._id.toString()] = currentLine.tunedInMemberIds;
});
updatePeerMap((draft) => {
// go through peer map
// if there is someone in it who is not in a tuned in line, then destroy peer and remove
Object.entries(draft).map(([lineId, lineStreamData]) => {
// if I left this channel, then I want to make sure to destroy and delete all relations
if (
roomsMap[lineId]?.tunedInMemberIds &&
!roomsMap[lineId]?.tunedInMemberIds.includes(user._id.toString())
) {
lineStreamData?.peerRelations.forEach((peerRelation) => {
peerRelation.peer.destroy();
});
delete draft[lineId];
return;
}
const usersToRemove = [];
lineStreamData?.peerRelations?.forEach((peerRelation) => {
if (!tunedUsersForLines[lineId].includes(peerRelation.userId)) {
peerRelation.peer.destroy();
usersToRemove.push(peerRelation.userId);
}
});
draft[lineId].peerRelations = draft[lineId]?.peerRelations?.filter(
(peerRelation) => !usersToRemove.includes(peerRelation.userId),
);
});
});
}, [roomsMap, updatePeerMap, user]);
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}
setLocalStreamForLine={setLocalStreamForLine}
/>
);
})}
{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,
setLocalStreamForLine,
}: {
lineId: string;
membersToCall: string[];
handleAddPeer: (lineId: string, userId: string, peerObj: Peer) => void;
setLocalStreamForLine: (lineId: string, localStreamForLine: 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);
// todo get the user media selections
// 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
navigator.mediaDevices
.getUserMedia({ video: videoConstraints, audio: true })
.then((localMediaStream: MediaStream) => {
setLocalStreamForLine(lineId, localMediaStream);
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');
// make sure this peer gets destroyed when it's time to remove this listener
const localPeerConnection = new Peer({
initiator: true,
stream: localMediaStream,
trickle: true, // 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);
});
localPeerConnection.on('stream', (remoteStream: MediaStream) => {
handleGotPeerRemoteStream(lineId, memberId, remoteStream);
});
});
});
});
return <></>;
}