diff --git a/packages/desktop/src/providers/ConversationProvider.tsx b/packages/desktop/src/providers/ConversationProvider.tsx index d149e67..276b64a 100644 --- a/packages/desktop/src/providers/ConversationProvider.tsx +++ b/packages/desktop/src/providers/ConversationProvider.tsx @@ -1,5 +1,6 @@ import { ConnectToLineRequest, + RtcCallRequest, ServerRequestChannels, ServerResponseChannels, SomeoneConnectedResponse, @@ -11,20 +12,21 @@ import { import Conversation, { MemberState } from '@nirvana/core/models/conversation.model'; import { ConversationMap, MasterConversation } from '../util/types'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { Updater, useImmer } from 'use-immer'; import { checkIfOnOnOneExists, createConversation, getConversationById, getConversations, } from '../api/NirvanaApi'; +import { useAsyncFn, useEffectOnce } from 'react-use'; import CreateConversationRequest from '@nirvana/core/requests/CreateConversationRequest.request'; +import Peer from 'simple-peer'; import { Typography } from '@mui/material'; import User from '@nirvana/core/models/user.model'; import toast from 'react-hot-toast'; -import { useAsyncFn } from 'react-use'; import useAuth from './AuthProvider'; -import { useImmer } from 'use-immer'; import useSockets from './SocketProvider'; // responsible for firing socket events from the local client @@ -326,6 +328,7 @@ export function ConversationProvider({ children }: { children: React.ReactNode } {userTunedInConversations.map((tunedConversation) => ( ))} @@ -337,13 +340,11 @@ export default function useConversations() { return React.useContext(ConversationContext); } -const videoConstraints = false; - -// { -// frameRate: 30, -// width: { max: 100 }, -// height: { max: 200 }, -// }; +const videoConstraints = { + frameRate: 15, + width: { max: 100 }, + height: { max: 200 }, +}; const iceServers = [ // { urls: 'stun:stun.l.google.com:19302' }, @@ -416,15 +417,25 @@ function useDevices() { // - unmount if you want to leave room // - have event listeners specific to this room // - take in new users that come into the room -function Room({ conversation }: { conversation: MasterConversation; mediaStream?: MediaStream }) { +function Room({ + conversation, + setConversationMap, +}: { + conversation: MasterConversation; + mediaStream?: MediaStream; + setConversationMap: Updater; +}) { const { user } = useAuth(); + const { $ws } = useSockets(); // have internal state to manage details of this "room" // ============== STREAMING =============== // handle incoming calls and accept calls and create objects for them - useEffect(() => { + useEffectOnce(() => { + const localPeersForRoom: Peer[] = []; + // ?will there be race condition where this room component is rendered but we don't have the latest // ?list of tunedin folks and so we may just end up calling select few? // ?in this case, start with initiating event to get all people in room first @@ -434,10 +445,98 @@ function Room({ conversation }: { conversation: MasterConversation; mediaStream? const allOtherUserIds = conversation.tunedInUsers.filter( (memberUserId) => memberUserId !== user._id.toString(), ); + toast.success('calling bunch of people'); - // initiate listeners for all of them + navigator.mediaDevices + .getUserMedia({ video: videoConstraints, audio: true }) + .then((localMediaStream: MediaStream) => { + // for each person, create peer object + allOtherUserIds.forEach((otherUserId) => { + const connectingToast = toast.loading('calling peer for a snappy experience'); + + // ========= PEER CREATION ============= + // 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, + }, + }); + + // ========= PEER EVENT HANDLERS ============= + localPeerConnection.on('signal', (signal) => { + console.log('have a signal to make call to someone '); + + $ws.emit( + ServerRequestChannels.RTC_CALL_SOMEONE_FOR_LINE, + new RtcCallRequest(otherUserId, conversation._id.toString(), signal), + ); + + toast.dismiss(connectingToast); + + // notifying globally that we are in the process now + setConversationMap((draft) => { + if (draft[conversation._id.toString()]) { + draft[conversation._id.toString()].room = { + ...(draft[conversation._id.toString()].room ?? {}), + otherUserId: { peer: localPeerConnection }, + }; + } + draft[conversation._id.toString()].isConnectingToRoom = true; + }); + }); + + localPeerConnection.on('stream', (remoteStream: MediaStream) => { + // globally updating conversation so that other views can render what they want + setConversationMap((draft) => { + if (draft[conversation._id.toString()].room[otherUserId]) { + toast.success('got stream from remote, going to add to our '); + draft[conversation._id.toString()].room[otherUserId].stream = remoteStream; + remoteStream.getTracks().forEach((track) => { + // TODO: add particular track to right place + }); + } + }); + }); + + localPeerConnection.on('connect', () => { + toast.success('successfully connected to another peer'); + }); + + localPeerConnection.on('track', (track, stream) => { + // TODO: add to room contents + + toast('a peer added a track to a stream'); + }); + + localPeerConnection.on('close', () => { + // the person will be removed from the tuned in list, but the connections here are decoupled from that flow + // we want to manage the room within the master conversation and remove it for ourselves + + // TODO: update the map to remove the user and remove + + toast.error('peer connection was closed'); + }); + + localPeerConnection.on('error', (err) => { + console.error(err); + toast.error('there was a problem with the peer connection'); + }); + + localPeersForRoom.push(localPeerConnection); + }); + }); + + return () => { + // go through all peer connections and destroy them + localPeersForRoom.forEach((peerConnection) => { + peerConnection.destroy(); + }); + }; } - }, []); + }); const handleJoinRoom = useCallback((roomId: string) => { // call up folks and then have listeners for the peer connection objects that we created diff --git a/packages/desktop/src/util/types.ts b/packages/desktop/src/util/types.ts index 93418f9..41eeeb1 100644 --- a/packages/desktop/src/util/types.ts +++ b/packages/desktop/src/util/types.ts @@ -16,9 +16,12 @@ export type MasterConversation = Conversation & { [userId: string]: { peer: Peer; stream?: MediaStream; - tracks?: MediaStreamTrack[]; + audioTrack?: MediaStreamTrack; + videoTrack?: MediaStreamTrack; + screenTrack?: MediaStreamTrack; }; }; + isConnectingToRoom?: boolean; // all of audio clips, links, media, etc. content?: ContentBlock[];