diff --git a/packages/restapi/src/lib/spaceV2/SpaceV2.ts b/packages/restapi/src/lib/spaceV2/SpaceV2.ts index fe64f1fbe..6fd41c99c 100644 --- a/packages/restapi/src/lib/spaceV2/SpaceV2.ts +++ b/packages/restapi/src/lib/spaceV2/SpaceV2.ts @@ -1,6 +1,7 @@ import { produce } from "immer"; import { join } from "./join"; +import { ISpaceInviteInputOptions, inviteToJoin } from "./inviteToJoin"; import Constants, { ENV } from "../constants"; import { EnvOptionsType, SignerType, SpaceDTO, SpaceV2Data, VideoCallStatus } from "../types"; @@ -28,13 +29,19 @@ export const initSpaceInfo: SpaceDTO = { export const initSpaceV2Data: SpaceV2Data = { spaceInfo: initSpaceInfo, + meta: { + initiator: { + address: '', + signal: null, + }, + }, local: { stream: null, audio: null, video: null, address: '', }, - incoming: [ + incomingPeerStreams: [ { stream: null, audio: null, @@ -44,6 +51,16 @@ export const initSpaceV2Data: SpaceV2Data = { retryCount: 0, }, ], + pendingPeerStreams: [ + { + stream: null, + audio: null, + video: null, + address: '', + status: VideoCallStatus.UNINITIALIZED, + retryCount: 0, + }, + ] }; export interface SpaceV2ConstructorType extends EnvOptionsType { @@ -132,6 +149,11 @@ export class SpaceV2 { return this.peerConnections.get(peerId); } + // Set a connected peer's peer connection by their ID + setPeerConnection(peerId: string, peerConnection: RTCPeerConnection) { + this.peerConnections.set(peerId, peerConnection); + } + // Get the list of all connected peer IDs getConnectedPeerIds(): string[] { return Array.from(this.peerConnections.keys()); @@ -155,7 +177,8 @@ export class SpaceV2 { */ } - async invite(options: any) { + async invite(options: ISpaceInviteInputOptions) { + await inviteToJoin.call(this, options); // Call the function with the current "this" /** * will contain logic to handle invites made by host to listener */ diff --git a/packages/restapi/src/lib/spaceV2/helpers/sendSpaceNotification.ts b/packages/restapi/src/lib/spaceV2/helpers/sendSpaceNotification.ts new file mode 100644 index 000000000..daea8ca5e --- /dev/null +++ b/packages/restapi/src/lib/spaceV2/helpers/sendSpaceNotification.ts @@ -0,0 +1,107 @@ +import Constants, { ENV } from '../../constants'; +import { getCAIPWithChainId } from '../../helpers'; +import { sendNotification } from '../../payloads'; +import { + NOTIFICATION_TYPE, + SPACE_ACCEPT_REQUEST_TYPE, + SPACE_DISCONNECT_TYPE, + SPACE_REQUEST_TYPE, + VIDEO_CALL_TYPE, +} from '../../payloads/constants'; +import { SignerType, VideoCallStatus } from '../../types'; + +interface CallDetailsType { + type: SPACE_REQUEST_TYPE | SPACE_ACCEPT_REQUEST_TYPE | SPACE_DISCONNECT_TYPE; + data: Record; +}; + +interface SpaceInfoType { + recipientAddress: string; + senderAddress: string; + spaceId: string; + signalData: any; + status: VideoCallStatus; + env?: ENV; + callDetails?: CallDetailsType; +} + +interface UserInfoType { + signer: SignerType; + chainId: number; + pgpPrivateKey: string; +} + +export interface SpaceDataType { + recipientAddress: string; + senderAddress: string; + spaceId: string; + signalData?: any; + status: VideoCallStatus; + callDetails?: CallDetailsType; +} + +const sendSpaceNotification = async ( + { signer, chainId, pgpPrivateKey }: UserInfoType, + { + recipientAddress, + senderAddress, + spaceId, + status, + signalData = null, + env = Constants.ENV.PROD, + callDetails + }: SpaceInfoType +) => { + try { + const spaceData: SpaceDataType = { + recipientAddress, + senderAddress, + spaceId, + signalData, + status, + callDetails + }; + + console.log('sendSpacelNotification', 'spaceData', spaceData); + + const senderAddressInCaip = getCAIPWithChainId(senderAddress, chainId); + const recipientAddressInCaip = getCAIPWithChainId( + recipientAddress, + chainId + ); + + const notificationText = `Space connection from ${senderAddress}`; + + const notificationType = NOTIFICATION_TYPE.TARGETTED; + + await sendNotification({ + senderType: 1, // for chat notification + signer, + pgpPrivateKey, + chatId: spaceId, + type: notificationType, + identityType: 2, + notification: { + title: notificationText, + body: notificationText, + }, + payload: { + title: 'SpaceConnection', + body: 'SpaceConnection', + cta: '', + img: '', + additionalMeta: { + type: `${VIDEO_CALL_TYPE.PUSH_SPACE}+1`, + data: JSON.stringify(spaceData), + }, + }, + recipients: recipientAddressInCaip, + channel: senderAddressInCaip, + env, + }); + } catch (err) { + console.log('Error occured while sending notification for spaces connection', err); + } +}; + +export default sendSpaceNotification; diff --git a/packages/restapi/src/lib/spaceV2/inviteToJoin.ts b/packages/restapi/src/lib/spaceV2/inviteToJoin.ts new file mode 100644 index 000000000..def523df5 --- /dev/null +++ b/packages/restapi/src/lib/spaceV2/inviteToJoin.ts @@ -0,0 +1,205 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import * as Peer from 'simple-peer'; +import { produce } from "immer"; + +import { SpaceV2 } from "./SpaceV2"; +import sendSpaceNotification from './helpers/sendSpaceNotification'; + +import { VideoCallStatus } from "../types"; +import { SPACE_REQUEST_TYPE } from '../payloads'; + +// imports from video +import getIncomingIndexFromAddress from "../video/helpers/getIncomingIndexFromAddress"; +import { getIceServerConfig } from "../video/helpers/getIceServerConfig"; +import isJSON from '../video/helpers/isJSON'; + + +export interface ISpaceInviteInputOptions { + senderAddress: string; + recipientAddress: string; + spaceId: string; + onReceiveMessage?: (message: string) => void; + retry?: boolean; + details?: { + type: SPACE_REQUEST_TYPE; + data: Record; + }; +}; + +export async function inviteToJoin( + this: SpaceV2, + options: ISpaceInviteInputOptions + ): Promise { + const { + senderAddress, + recipientAddress, + spaceId, + onReceiveMessage = (message: string) => { + console.log('received a meesage', message); + }, + retry = false, + details, + } = options || {}; + + console.log('request', 'options', options); + + const recipientAddresses = Array.isArray(recipientAddress) + ? recipientAddress + : [recipientAddress]; + + for (const recipientAddress of recipientAddresses) { + try { + // set videoCallInfo state with status 1 (call initiated) + this.setSpaceV2Data((oldData) => { + return produce(oldData, (draft) => { + draft.local.address = senderAddress; + draft.spaceInfo.spaceId = spaceId; + draft.meta.initiator.address = senderAddress; + + const incomingIndex = getIncomingIndexFromAddress( + oldData.pendingPeerStreams, + recipientAddress + ); + + if (incomingIndex === -1) { + draft.pendingPeerStreams.push({ + stream: null, + audio: null, + video: null, + address: recipientAddress, + status: retry + ? VideoCallStatus.RETRY_INITIALIZED + : VideoCallStatus.INITIALIZED, + retryCount: retry ? 1 : 0, + }); + } else { + draft.pendingPeerStreams[incomingIndex].address = recipientAddress; + draft.pendingPeerStreams[incomingIndex].status = retry + ? VideoCallStatus.RETRY_INITIALIZED + : VideoCallStatus.INITIALIZED; + draft.pendingPeerStreams[incomingIndex].retryCount += retry ? 1 : 0; + } + }); + }); + + // fetching the iceServers config + const iceServerConfig = await getIceServerConfig(this.env); + const peerConnection = new Peer({ + initiator: true, + trickle: false, + stream: this.data.local.stream, + config: { + iceServers: iceServerConfig, + }, + }); + + this.setPeerConnection(recipientAddress, peerConnection); + + peerConnection.on('signal', (data: any) => { + this.setSpaceV2Data((oldData) => { + return produce(oldData, (draft) => { + draft.meta.initiator.signal = data; + }); + }); + + // sending notification to the recipientAddress with video call signaling data + sendSpaceNotification( + { + signer: this.signer, + chainId: this.chainId, + pgpPrivateKey: this.pgpPrivateKey, + }, + { + senderAddress, + recipientAddress, + status: retry + ? VideoCallStatus.RETRY_INITIALIZED + : VideoCallStatus.INITIALIZED, + spaceId, + signalData: data, + env: this.env, + callDetails: details, + } + ); + }); + + peerConnection.on('connect', () => { + peerConnection.send( + `initial message from ${senderAddress}` + ); + peerConnection.send( + JSON.stringify({ + type: 'isVideoOn', + value: this.data.local.video, + }) + ); + peerConnection.send( + JSON.stringify({ + type: 'isAudioOn', + value: this.data.local.audio, + }) + ); + }); + + peerConnection.on('data', (data: any) => { + if (isJSON(data)) { + const parsedData = JSON.parse(data); + + if (parsedData.type === 'isVideoOn') { + console.log('IS VIDEO ON', parsedData.value); + this.setSpaceV2Data((oldData) => { + return produce(oldData, (draft) => { + const incomingIndex = getIncomingIndexFromAddress( + oldData.pendingPeerStreams, + recipientAddress + ); + draft.pendingPeerStreams[incomingIndex].video = parsedData.value; + }); + }); + } + + if (parsedData.type === 'isAudioOn') { + console.log('IS AUDIO ON', parsedData.value); + this.setSpaceV2Data((oldData) => { + return produce(oldData, (draft) => { + const incomingIndex = getIncomingIndexFromAddress( + oldData.pendingPeerStreams, + recipientAddress + ); + draft.pendingPeerStreams[incomingIndex].audio = parsedData.value; + }); + }); + } + } + }); + + peerConnection.on( + 'stream', + (currentStream: MediaStream) => { + console.log('received incoming stream', currentStream); + const pendingIndex = getIncomingIndexFromAddress( + this.data.pendingPeerStreams, + recipientAddress + ); + + // Here, we can handle if we want to merge stream or anything + // this.onReceiveStream( + // currentStream, + // recipientAddress, + // this.data.pendingPeerStreams[pendingIndex].audio + // ); + + this.setSpaceV2Data((oldData) => { + return produce(oldData, (draft) => { + draft.incomingPeerStreams.push(draft.pendingPeerStreams[pendingIndex]); + draft.pendingPeerStreams.splice(pendingIndex, 1); + }); + }); + } + ); + } catch (err) { + console.log('error in invite', err); + } + } + } diff --git a/packages/restapi/src/lib/types/index.ts b/packages/restapi/src/lib/types/index.ts index 293b95c88..0ea63ead4 100644 --- a/packages/restapi/src/lib/types/index.ts +++ b/packages/restapi/src/lib/types/index.ts @@ -680,13 +680,20 @@ export type VideoCallData = { export type SpaceV2Data = { spaceInfo: SpaceDTO; + meta: { + initiator: { + address: string; + signal: any; + }; + }; local: { stream: IMediaStream; audio: boolean | null; video: boolean | null; address: string; }; - incoming: PeerData[]; + incomingPeerStreams: PeerData[]; + pendingPeerStreams: PeerData[]; } export type VideoCreateInputOptions = {