import { RequestHandler } from "../entities/RequestHandler"; import { UserWebSocketRequestsHandler } from "../entities/WebSocketRequestHandler"; import { ActiveClient, UserWebSocketController } from "./UserWebSocketController"; import { EventsHelper, CallSignalFromRTCRelayEvent } from "../helpers/EventsHelper"; import * as ws from 'ws' import { WsMessage } from "../entities/WsMessage"; import { conf } from "../helpers/ConfigHelper"; import { RTCRelayController } from "./RTCRelayController"; /** * Legacy calls controller * * @author Pierre HUBERT */ export class CallsController { /** * Legacy calls config */ public static GetLegacyConfig(h: RequestHandler) { h.send({ enabled: false }); } /** * Get call configuration * * @param h Request handler */ public static async GetConfig(h: UserWebSocketRequestsHandler) { // Check if user is member of at least one call if(h.wsClient.activeCalls.size == 0) h.error(401, "You do not belong to any call for now!") h.send({ iceServers: conf().rtc_relay.iceServers }) } /** * Join a call * * @param h Request handler */ public static async JoinCall(h: UserWebSocketRequestsHandler) { const convID = await h.postConversationId("convID"); // If the user was active in any other calls, remove him for(const c of UserWebSocketController.active_clients.filter((f) => f.userID == h.getUserId() && f.activeCalls.has(convID))) await this.MakeUserLeaveCall(convID, c) h.wsClient.activeCalls.set(convID, { ready: false }); // Notify all other users await UserWebSocketController.SendToSpecifcClients( (c) => c.activeCalls.has(convID) && c.userID != h.getUserId(), () => WsMessage.NoIDMessage("user_joined_call", { callID: convID, userID: h.getUserId() }) ) h.success(); } /** * Leave a call * * @param h Request handler */ public static async LeaveCall(h: UserWebSocketRequestsHandler) { // Warning ! For some technical reasons, we do not check if the user // really belongs to the conversation, so be careful when manipulating // conversation ID here const convID = h.postInt("convID"); // Check if user is already in the conversation if(!h.wsClient.activeCalls.has(convID)) { h.success(); return; } // Make the user leave the call await this.MakeUserLeaveCall(convID, h.wsClient); h.success(); } /** * Get the list of members for a call * * The user must have joined the call to get this information * * @param h Request handler */ public static async GetMembersList(h: UserWebSocketRequestsHandler) { const convID = h.postCallId("callID"); h.send(UserWebSocketController.active_clients.filter( (f) => f.activeCalls.has(convID) ).map(f => {return { userID: f.userID, ready: f.activeCalls.get(convID).ready }})) } /** * Handles client signal * * @param h Request handler */ public static async OnClientSignal(h: UserWebSocketRequestsHandler) { const callID = h.postCallId("callID"); const peerID = h.postCallPeerID(callID, "peerID"); // The ID of the user we stream the audio / video from const type = h.postString("type"); const data = h.postJSON("data"); if(type !== "SDP" && type !== "CANDIDATE") h.error(401, "Invalid candidate type"); if(type == "SDP" && (!data.hasOwnProperty("type") || data.type !== "offer" || !data.hasOwnProperty("sdp"))) h.error(401, "Invalid SDP signal!") if(type == "CANDIDATE" && (!data.hasOwnProperty("candidate") || !data.hasOwnProperty("sdpMLineIndex") || !data.hasOwnProperty("sdpMid"))) h.error(401, "Invalid candidate signal!") await RTCRelayController.SendMessage({ title: "signal", callHash: callID+"-"+peerID, peerId: String(peerID === h.getUserId() ? 0 : peerID), data: { type: type, data: data } }); h.success() } /** * Handles proxy signals * * @param e Event information */ public static async OnProxySignal(e: CallSignalFromRTCRelayEvent) { // Extract information const callID = Number(e.callHash.split("-")[0]) const peerID = Number(e.callHash.split("-")[1]) let targetUser = Number(e.peerID) if(targetUser == 0) targetUser = peerID; // Send the message await UserWebSocketController.SendToSpecifcClients( (c) => c.userID == targetUser && c.activeCalls.has(callID), () => WsMessage.NoIDMessage("new_call_signal", { callID: callID, peerID: peerID, data: e.data }) ) } /** * Mark a user as ready to share its streams * * @param h Request handler */ public static async MarkUserReady(h: UserWebSocketRequestsHandler) { const callID = h.postCallId("callID"); h.wsClient.activeCalls.get(callID).ready = true; // Notify all other users await UserWebSocketController.SendToSpecifcClients( (c) => c.activeCalls.has(callID) && c.userID != h.getUserId(), () => WsMessage.NoIDMessage("user_ready", { callID: callID, userID: h.getUserId() }) ) h.success(); } /** * Make the client leave the call * * @param c Client information */ public static async MakeUserLeaveCall(convID: number, c: ActiveClient) { c.activeCalls.delete(convID) // Notify user (if possible) if(c.ws.readyState == ws.OPEN) UserWebSocketController.SendToClient(c, WsMessage.NoIDMessage("call_closed",convID)); // Notify all other users await UserWebSocketController.SendToSpecifcClients( (c) => c.activeCalls.has(convID), () => WsMessage.NoIDMessage("user_left_call", { callID: convID, userID: c.userID }) ) } } // Listen for websocket closed EventsHelper.Listen("user_ws_closed", async w => { for(const convID of w.client.activeCalls) await CallsController.MakeUserLeaveCall(convID[0], w.client) }); // Listen to signal from RTC proxy EventsHelper.Listen("rtc_relay_signal", async msg => { await CallsController.OnProxySignal(msg) }) // Close all call when RTC WS is closed EventsHelper.Listen("rtc_relay_ws_closed", async () => { for(const client of UserWebSocketController.active_clients) { for(const convID of client.activeCalls) await CallsController.MakeUserLeaveCall(convID[0], client); } })