diff --git a/packages/api/sockets/index.ts b/packages/api/sockets/index.ts index d66ea2c..306ccc0 100644 --- a/packages/api/sockets/index.ts +++ b/packages/api/sockets/index.ts @@ -1,5 +1,9 @@ import { ConnectToLineRequest, + RtcAnswerRequest, + RtcCallRequest, + RtcNewUserResponse, + RtcReceiveAnswerResponse, ServerRequestChannels, ServerResponseChannels, SomeoneConnectedResponse, @@ -32,6 +36,9 @@ const config = loadConfig(); const socketIdsToUserIds: { [socketId: string]: string; } = {}; +const userIdsToSocketIds: { + [userId: string]: string; +} = {}; export default function InitializeWs(io: any) { console.log("initializing web sockets"); @@ -62,6 +69,7 @@ export default function InitializeWs(io: any) { const userInfo: JwtClaims = socket.userInfo; socketIdsToUserIds[socket.id] = userInfo.userId.toString(); + userIdsToSocketIds[userInfo.userId.toString()] = socket.id; console.log( `a user connected | user Id: ${userInfo.userId} and name: ${userInfo.name}` @@ -231,9 +239,35 @@ export default function InitializeWs(io: any) { // ); // }); + // tell the proper other user to create a local peer object for the one on one mesh connection + socket.on( + ServerRequestChannels.RTC_CALL_REQUEST, + (req: RtcCallRequest) => { + const userSocketId = userIdsToSocketIds[req.userIdToCall]; + + io.to(userSocketId).emit( + `${ServerResponseChannels.RTC_NEW_USER_JOINED_RESPONSE_PREFIX}:${req.lineId}`, + new RtcNewUserResponse(userInfo.userId, req.simplePeerSignal) + ); + } + ); + + socket.on( + ServerRequestChannels.RTC_ANSWER_REQUEST, + (req: RtcAnswerRequest) => { + const userSocketId = userIdsToSocketIds[req.userIdToCall]; + + io.to(userSocketId).emit( + `${ServerResponseChannels.RTC_RECEIVING_ANSWER_RESPONSE_PREFIX}:${req.lineId}`, + new RtcReceiveAnswerResponse(userInfo.userId, req.simplePeerSignal) + ); + } + ); + // ==== DISCONNECT ==== socket.on("disconnect", () => { delete socketIdsToUserIds[socket.id]; + delete userIdsToSocketIds[userInfo.userId]; // get all of the rooms of this socket // notify everyone of this disconnection @@ -242,6 +276,10 @@ export default function InitializeWs(io: any) { console.log( `list of sockets mappings in memory: ${socketIdsToUserIds}` ); + + console.log(socket.rooms); + + // ! NOTIFY ALL CONNECTED LINES SO THAT THEY CAN REMOVE FROM THEIR SESSION CONNECTED USERS ARRAY AND TUNED IN ARRAY }); }); } diff --git a/packages/core/sockets/channels.ts b/packages/core/sockets/channels.ts index d1542ab..751fb68 100644 --- a/packages/core/sockets/channels.ts +++ b/packages/core/sockets/channels.ts @@ -41,6 +41,9 @@ export enum ServerRequestChannels { BROADCAST_TO_LINE = "BROADCAST_TO_LINE", STOP_BROADCAST_TO_LINE = "STOP_BROADCAST_TO_LINE", + + RTC_CALL_REQUEST = "RTC_CALL_PREFIX", + RTC_ANSWER_REQUEST = "RTC_ANSWER_REQUEST_PREFIX", } export enum ServerResponseChannels { @@ -52,6 +55,10 @@ export enum ServerResponseChannels { SOMEONE_STARTED_BROADCASTING = "SOMEONE_STARTED_BROADCASTING", //show their stream tracks SOMEONE_STOPPED_BROADCASTING = "SOMEONE_STOPPED_BROADCASTING", // stop showing their stream tracks + + // sending to the correct room of tunedin folks AND also making sure it's the right event handler in the right handler for this component + RTC_NEW_USER_JOINED_RESPONSE_PREFIX = "RTC_NEW_USER_JOINED_RESPONSE_PREFIX", + RTC_RECEIVING_ANSWER_RESPONSE_PREFIX = "RTC_RECEIVING_ANSWER_RESPONSE_PREFIX", } export default SocketChannels; @@ -107,3 +114,27 @@ export class UserStoppedBroadcastingResponse { export class SocketEmitter { constructor(public channel: SocketChannels, data: T) {} } + +export class RtcCallRequest { + constructor( + public lineId: string, + public userIdToCall: string, + public simplePeerSignal: any + ) {} +} + +export class RtcNewUserResponse { + constructor(public newUserId: string, public simplePeerSignal: any) {} +} + +export class RtcAnswerRequest { + constructor( + public lineId: string, + public userIdToCall: string, + public simplePeerSignal: any + ) {} +} + +export class RtcReceiveAnswerResponse { + constructor(public answererUserId: string, public simplePeerSignal: any) {} +} diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 9aa0d37..db88168 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -55,8 +55,8 @@ [ "@electron-forge/plugin-webpack", { - "port": "4000", - "loggerPort": "9001", + "port": "4002", + "loggerPort": "9005", "mainConfig": "./webpack.main.config.js", "devContentSecurityPolicy": "connect-src 'self' http://localhost:5000 ws://localhost:5000 'unsafe-eval'", "renderer": { diff --git a/packages/desktop/src/components/lines/lineRow.tsx/index.tsx b/packages/desktop/src/components/lines/lineRow.tsx/index.tsx index 5c90942..928e662 100644 --- a/packages/desktop/src/components/lines/lineRow.tsx/index.tsx +++ b/packages/desktop/src/components/lines/lineRow.tsx/index.tsx @@ -1,5 +1,9 @@ import { FiActivity, FiSun } from "react-icons/fi"; import { + RtcAnswerRequest, + RtcCallRequest, + RtcNewUserResponse, + RtcReceiveAnswerResponse, ServerResponseChannels, SomeoneUntunedFromLineResponse, } from "@nirvana/core/sockets/channels"; @@ -11,6 +15,7 @@ import LineIcon from "../lineIcon"; import { LineMemberState } from "@nirvana/core/models/line.model"; import MasterLineData from "@nirvana/core/models/masterLineData.model"; import Peer from "simple-peer"; +import { ServerRequestChannels } from "../../../../../core/sockets/channels"; import moment from "moment"; import toast from "react-hot-toast"; import { useGetUserDetails } from "../../../controller/index"; @@ -157,6 +162,7 @@ export default function LineRow({ {/* mounts and unmounts based on if in the room or now */} {isUserTunedIn && ( @@ -165,10 +171,14 @@ export default function LineRow({ ); } +type PeerMap = { [userId: string]: Peer }; + function StreamRoom({ + lineId, tunedInUsers, currentBroadcasters, }: { + lineId: string; tunedInUsers?: string[]; currentBroadcasters?: string[]; }) { @@ -176,65 +186,152 @@ function StreamRoom({ // local stream specifically for this stream room const [localStream, setLocalStream] = useState(); const userStreamTagRef = useRef(null); + const { data: userDetails } = useGetUserDetails(); // local peer map of userIds to peers - const [userPeers, setUserPeers] = useState<{ [userId: string]: Peer }>({}); + const [userPeers, setUserPeers] = useState({}); // ws listen to events of user disconnecting and such or rely on tunedin members prop const { $ws } = useLineDataProvider(); useEffect(() => { - navigator.mediaDevices - .getUserMedia({ - video: false, - audio: { - echoCancellation: true, - autoGainControl: true, - }, - }) - .then((userStream) => { - setLocalStream(userStream); + if (userDetails) { + navigator.mediaDevices + .getUserMedia({ + video: false, + audio: { + echoCancellation: true, + autoGainControl: true, + }, + }) + .then((userStream) => { + setLocalStream(userStream); - // if (userStreamTagRef?.current) - // userStreamTagRef.current.srcObject = userStream; + // if (userStreamTagRef?.current) + // userStreamTagRef.current.srcObject = userStream; - // alternative to dom element - // const audio = new Audio(); - // audio.autoplay = true; - // audio.srcObject = userStream; + // alternative to dom element + // const audio = new Audio(); + // audio.autoplay = true; + // audio.srcObject = userStream; - // test distortion to go away on disabling audio - setTimeout(() => { - console.log("stopping audio stream "); + // test distortion to go away on disabling audio + setTimeout(() => { + console.log("stopping audio stream "); - userStream.getTracks().forEach((track) => { - track.enabled = !track.enabled; + userStream.getTracks().forEach((track) => { + track.enabled = !track.enabled; - track.stop(); + track.stop(); + }); + }, 2000); + + // take the initial list of tunedInUsers without my own id + const everyOtherTunedUserId = tunedInUsers.filter( + (tunedUserId) => tunedUserId !== userDetails?.user._id.toString() + ); + + const localPeerConnections: PeerMap = {}; + + everyOtherTunedUserId.forEach((otherTunedInUserId) => { + // create local peer objects for them + const localPeerInitiator = new Peer({ + initiator: true, + stream: userStream, + trickle: false, // prevents the multiple tries on different ice servers and signal from getting called a bunch of times + }); + + // set it for our map that we will iterate through to display streams in other child view + localPeerConnections[otherTunedInUserId] = localPeerInitiator; + + // notify each one with specific signal + localPeerInitiator.on("signal", (signal) => { + $ws.emit( + ServerRequestChannels.RTC_CALL_REQUEST, + new RtcCallRequest(lineId, otherTunedInUserId, signal) + ); + }); }); - }, 2000); - // take the initial list of tunedInUsers - // create local peer objects for them - // notify all of them individually - // - }) - .catch((error) => { - console.error(error); + setUserPeers(localPeerConnections); - toast.error( - "Make sure that you have permissions enabled and microphone connected" - ); - }); + // answer calls + $ws.on( + `${ServerResponseChannels.RTC_NEW_USER_JOINED_RESPONSE_PREFIX}:${lineId}`, + (res: RtcNewUserResponse) => { + // create a local peer connection for this new user - $ws.on( - ServerResponseChannels.SOMEONE_UNTUNED_FROM_LINE, - (res: SomeoneUntunedFromLineResponse) => { - // take the relevant userId - res.userId; - } - ); - }, []); + console.log( + "ooo newbie joined room, I guess I will accept it and send him my signal" + ); + + var 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: localStream, // add in my own stream that I got before + }); + + peerForMeAndNewbie.on("signal", (signal) => { + console.log( + "as the answerer, I am going to send back my signal so that the newbie can update his local peer for me" + ); + $ws.emit( + ServerRequestChannels.RTC_ANSWER_REQUEST, + new RtcAnswerRequest(lineId, res.newUserId, signal) + ); + }); + + peerForMeAndNewbie.signal(res.simplePeerSignal); + + setUserPeers((prevUsersPeers) => { + const newUserPeers = { ...prevUsersPeers }; + + newUserPeers[res.newUserId] = peerForMeAndNewbie; + + return newUserPeers; + }); + } + ); + + // take care of answers recevied + $ws.on( + `${ServerResponseChannels.RTC_RECEIVING_ANSWER_RESPONSE_PREFIX}:${lineId}`, + (res: RtcReceiveAnswerResponse) => { + console.log("oooo some master received my call and accepted it"); + + // find the peer we created earlier for this master + // ?is this okay? using the setter to get the current state? + setUserPeers((previousUserPeersMap) => { + const peerForAnswerer = + previousUserPeersMap[res.answererUserId]; + + if (peerForAnswerer) { + peerForAnswerer.signal(res.simplePeerSignal); + } else { + console.error( + "could not find the peer we created before for this master" + ); + } + + return previousUserPeersMap; + }); + } + ); + }) + .catch((error) => { + console.error(error); + + toast.error( + "Make sure that you have permissions enabled and microphone connected" + ); + }); + } + }, [userDetails, setUserPeers]); + + // calculate diff to clean our userPeerMap to unmount and detroy certain peer connections + useEffect(() => {}, [tunedInUsers]); + + // todo, someone tell the main object that I am finally connected after everything...different than tuned in useEffect(() => { console.log("change in tuned in users in the streaming room!!!"); @@ -245,13 +342,14 @@ function StreamRoom({ <>