UNPKG

msg91-webrtc-call

Version:

**msg91-webrtc-call** is a lightweight JavaScript SDK that enables you to easily add peer-to-peer WebRTC audio/video calling functionality to your web applications using the MSG91 infrastructure.

518 lines (517 loc) 19.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.WebRTC_EVENT = exports.CALL_EVENT = void 0; const rtlayer_1 = require("./config/rtlayer"); const media_server_1 = require("./service/media-server"); const mediasoup_client_1 = require("mediasoup-client"); const util_1 = require("./util"); class EventEmitter { constructor() { this.eventListeners = {}; } on(event, callback) { if (!this.eventListeners[event]) { this.eventListeners[event] = []; } this.eventListeners[event].push(callback); return { remove: () => { this.removeListener(event, callback); } }; } emit(event, data) { const listeners = this.eventListeners[event]; if (listeners) { for (const listener of listeners.slice()) { listener(data); } } } removeListener(channel, listener) { const listeners = this.eventListeners[channel] || []; const index = listeners.indexOf(listener); if (index !== -1) { listeners.splice(index, 1); } this.eventListeners[channel] = listeners; } } var CALL_EVENT; (function (CALL_EVENT) { CALL_EVENT["ENDED"] = "ended"; CALL_EVENT["ANSWERED"] = "answered"; CALL_EVENT["REJOINED"] = "rejoined"; CALL_EVENT["UNAVAILABLE"] = "unavailable"; CALL_EVENT["ERROR"] = "error"; CALL_EVENT["CONNECTED"] = "connected"; CALL_EVENT["MUTE"] = "mute"; CALL_EVENT["UNMUTE"] = "unmute"; CALL_EVENT["MESSAGE"] = "message"; })(CALL_EVENT || (exports.CALL_EVENT = CALL_EVENT = {})); var CALL_TYPE; (function (CALL_TYPE) { CALL_TYPE["INCOMING"] = "incoming-call"; CALL_TYPE["OUTGOING"] = "outgoing-call"; })(CALL_TYPE || (CALL_TYPE = {})); var USER_STATUS; (function (USER_STATUS) { USER_STATUS["IDLE"] = "idle"; USER_STATUS["BUSY"] = "busy"; // User is currently on a call })(USER_STATUS || (USER_STATUS = {})); var RINGTONE; (function (RINGTONE) { RINGTONE["STOP"] = "stop"; RINGTONE["RING"] = "ring"; })(RINGTONE || (RINGTONE = {})); var CALL_MANAGER_EVENT; (function (CALL_MANAGER_EVENT) { CALL_MANAGER_EVENT["RINGTONE_STATUS_CHANGED"] = "ringtone-status-changed"; })(CALL_MANAGER_EVENT || (CALL_MANAGER_EVENT = {})); class CallManager extends EventEmitter { constructor() { super(); this.calls = new Map(); this.callListeners = new Map(); this.userStatus = USER_STATUS.IDLE; this.ringtoneStatus = RINGTONE.STOP; } addCall(call) { const info = call.getInfo(); const callId = info.id; this.calls.set(callId, call); this.updateUserStatus(); this.updateRingtoneStatus(this.userStatus); const callConnectedListener = call.on(CALL_EVENT.CONNECTED, () => { // Update the user status and ringtone status this.updateUserStatus(); this.updateRingtoneStatus(this.userStatus); }); const callEndedListener = call.on(CALL_EVENT.ENDED, () => { // Cleanup the state for the call this.updateUserStatus(); this.updateRingtoneStatus(this.userStatus); this.removeCall(callId); }); // Only for incoming calls const callUnavailableListener = call.on(CALL_EVENT.UNAVAILABLE, () => { // Mark the call as unavailable this.updateUserStatus(); this.updateRingtoneStatus(this.userStatus); this.removeCall(callId); }); const callRejoinedListener = call.on(CALL_EVENT.REJOINED, () => { this.updateUserStatus(); this.updateRingtoneStatus(this.userStatus); // User rejoined the call }); // Only for outgoing calls const callAnsweredListener = call.on(CALL_EVENT.ANSWERED, () => { this.updateUserStatus(); this.updateRingtoneStatus(this.userStatus); }); const callErrorListener = call.on(CALL_EVENT.ERROR, () => { // Cleanup the state for the call this.updateUserStatus(); this.updateRingtoneStatus(this.userStatus); }); this.callListeners.set(callId, [callConnectedListener, callEndedListener, callUnavailableListener, callRejoinedListener, callAnsweredListener, callErrorListener]); } removeCall(callId) { this.calls.delete(callId); const listeners = this.callListeners.get(callId) || []; listeners.forEach(listener => listener === null || listener === void 0 ? void 0 : listener.remove()); this.callListeners.delete(callId); } updateUserStatus() { // Check if any call is in CONNECTED state const isBusy = Array.from(this.calls.values()).some(call => call.getStatus() === CALL_STATUS.CONNECTED); const currentStatus = isBusy ? USER_STATUS.BUSY : USER_STATUS.IDLE; if (this.userStatus !== currentStatus) { this.userStatus = currentStatus; console.info("User status changed to:", this.userStatus); } } updateRingtoneStatus(userStatus = this.userStatus) { // Check if any incoming call is in RINGING state const shouldRing = Array.from(this.calls.values()).some(call => call.getStatus() === CALL_STATUS.RINGING && call.getInfo().type === CALL_TYPE.INCOMING); const isUserBusy = userStatus === USER_STATUS.BUSY; const currentRingtoneStatus = (shouldRing && !isUserBusy) ? RINGTONE.RING : RINGTONE.STOP; if (this.ringtoneStatus != currentRingtoneStatus) { this.ringtoneStatus = currentRingtoneStatus; console.info("Ringtone status changed to:", this.ringtoneStatus); this.emit(CALL_MANAGER_EVENT.RINGTONE_STATUS_CHANGED, this.ringtoneStatus); } } getCall(callId) { return this.calls.get(callId); } } var WebRTC_EVENT; (function (WebRTC_EVENT) { WebRTC_EVENT["CALL"] = "call"; WebRTC_EVENT["INCOMING_CALL"] = "incoming-call"; WebRTC_EVENT["OUTGOING_CALL"] = "outgoing-call"; WebRTC_EVENT["PLAY_RINGTONE"] = "play-ringtone"; WebRTC_EVENT["STOP_RINGTONE"] = "stop-ringtone"; })(WebRTC_EVENT || (exports.WebRTC_EVENT = WebRTC_EVENT = {})); class WebRTC extends EventEmitter { constructor(userToken) { super(); this.status = 'idle'; this.callManager = new CallManager(); this.user = {}; this.userToken = userToken; this.callManager.on(CALL_MANAGER_EVENT.RINGTONE_STATUS_CHANGED, (status) => { if (status == RINGTONE.RING) { this.emit(WebRTC_EVENT.PLAY_RINGTONE, {}); } else { this.emit(WebRTC_EVENT.STOP_RINGTONE, {}); } }); (0, media_server_1.getUserData)(userToken).then((user) => { this.user.id = user === null || user === void 0 ? void 0 : user.id; this.user.name = user === null || user === void 0 ? void 0 : user.name; rtlayer_1.default.on(`user:${this.user.id}:call`, (data) => { data = JSON.parse(data); let call = null; data.token = this.callToken || data.callToken; switch (data.type) { case 'incoming-call': call = new IncommingCall(this.user, userToken, data); if (call) this.emit(WebRTC_EVENT.INCOMING_CALL, call); break; case 'outgoing-call': if (this.callId != data.id) break; call = new OutgoingCall(this.user, userToken, data); if (call) this.emit(WebRTC_EVENT.OUTGOING_CALL, call); break; default: console.error("Invalid call type"); break; } if (call) this.callManager.addCall(call); if (call) this.emit(WebRTC_EVENT.CALL, call); }); console.log("USER REGISTERED : ", this.user); }).catch((error) => { console.error("USER REGISTRATION UN-SUCCESSFUL", error); }); } close() { rtlayer_1.default.unsubscribe(`user:${this.user.id}:call`); } async call(callToken) { let retryCount = 0; while (!rtlayer_1.default.isOpen() && retryCount++ < 10) { await new Promise(resolve => setTimeout(resolve, 300)); } this.callToken = callToken; const { id } = await (0, media_server_1.startNewCall)(callToken); this.status = 'calling'; this.callId = id; } async rejoinCall(callId) { this.callId = callId; let retryCount = 0; while (!rtlayer_1.default.isOpen() && retryCount++ < 10) { await new Promise(resolve => setTimeout(resolve, 300)); } const { id, message, error } = await (0, media_server_1.rejoinCall)(callId, this.userToken); this.status = 'calling'; this.callId = id; if (error) { throw new Error(message); } } async sendUserContext(data) { if (!data) return; try { await (0, media_server_1.sendUserContext)(this.userToken, Object.assign({ callId: this.callId }, data)); } catch (error) { console.error("Error sending user data:", error); } } } var CALL_STATUS; (function (CALL_STATUS) { CALL_STATUS["IDLE"] = "idle"; CALL_STATUS["RINGING"] = "ringing"; CALL_STATUS["CONNECTED"] = "connected"; CALL_STATUS["ENDED"] = "ended"; })(CALL_STATUS || (CALL_STATUS = {})); class Call extends EventEmitter { constructor(user, userToken, data) { super(); this.existingCall = false; this.status = CALL_STATUS.IDLE; this.audioTrack = null; this.rtlayerEvents = []; this.setStatus(CALL_STATUS.RINGING); this.userToken = userToken; this.user = user; this.id = data.id; this.from = data.from; this.to = data.to; this.type = data.type; this.producerTransport = data.producerTransport; this.consumerTransport = data.consumerTransport; this.routerRtpCapabilities = data.routerRtpCapabilities; this.device = new mediasoup_client_1.Device(); this.mediaStream = new MediaStream(); this.existingCall = (data === null || data === void 0 ? void 0 : data.status) == 'connected' ? true : false; } addEvent(callback) { this.rtlayerEvents.push(callback); } async setStatus(status) { this.status = status; } getStatus() { return this.status; } getMediaStream() { return this.mediaStream; } isExistingCall() { return this.existingCall; } mute() { if (this.audioTrack) { this.audioTrack.enabled = false; this.emit(CALL_EVENT.MUTE, { uid: this.user.id }); } } unmute() { if (this.audioTrack) { this.audioTrack.enabled = true; this.emit(CALL_EVENT.UNMUTE, { uid: this.user.id }); } } // Disconnect the connected call hang(data) { var _a; (_a = this.audioTrack) === null || _a === void 0 ? void 0 : _a.stop(); this.audioTrack = null; this.mediaStream.getTracks().forEach(track => track === null || track === void 0 ? void 0 : track.stop()); this.mediaStream = new MediaStream(); this.setStatus(CALL_STATUS.ENDED); const eventData = data || this.getInfo(); this.emit(CALL_EVENT.ENDED, eventData); (0, media_server_1.endCall)(this.id, this.userToken); if (this.producer) this.producer.close(); setTimeout(() => { this.rtlayerEvents.forEach((event) => event === null || event === void 0 ? void 0 : event.remove()); }, 2000); } getInfo() { return { id: this.id, from: this.from, to: this.to, type: this.type, }; } async setupCall() { await this.device.load({ routerRtpCapabilities: this.routerRtpCapabilities }); const pTransport = this.device.createSendTransport(this.producerTransport); const cTransport = this.device.createRecvTransport(this.consumerTransport); pTransport.on('connect', async ({ dtlsParameters }, callback, errback) => { try { console.log("----------> producer transport has connected"); // Notify the server that the transport is ready to connect with the provided DTLS parameters // socket.emit("connectProducerTransport", { dtlsParameters }); await (0, media_server_1.joinCall)(this.user.id, this.id, dtlsParameters, "producer"); // Callback to indicate success callback(); } catch (error) { // Errback to indicate failure errback(error); } }); pTransport.on("produce", async (parameters, callback, errback) => { const { kind, rtpParameters } = parameters; console.log("----------> transport-produce"); try { // Notify the server to start producing media with the provided parameters const response = await (0, media_server_1.startProducing)(this.user.id, this.id, kind, rtpParameters); console.log(response); callback({ id: response.id }); // socket.emit( // "transport-produce", // { kind, rtpParameters }, // ({ id }: any) => { // // Callback to provide the server-generated producer ID back to the transport // callback({ id }); // } // ); } catch (error) { // Errback to indicate failure errback(error); } }); cTransport.on('connect', async ({ dtlsParameters }, callback, errback) => { try { console.log("----------> consumer transport has connected"); // Notify the server that the transport is ready to connect with the provided DTLS parameters // socket.emit("connectProducerTransport", { dtlsParameters }); await (0, media_server_1.joinCall)(this.user.id, this.id, dtlsParameters, "consumer"); // Callback to indicate success callback(); } catch (error) { // Errback to indicate failure errback(error); } }); const consumerEvents = rtlayer_1.default.on(`user:${this.user.id}:call:${this.id}:consume`, async (data) => { data = JSON.parse(data); let consumer = await cTransport.consume({ id: data.id, producerId: data.producerId, kind: data.kind, rtpParameters: data.rtpParameters }); console.log("----------> consumer created"); if (!consumer) return; const { track } = consumer; console.log("TRACK", track); this.mediaStream.addTrack(track); this.emit(CALL_EVENT.CONNECTED, this.mediaStream); }); this.rtlayerEvents.push(consumerEvents); try { let stream = await navigator.mediaDevices.getUserMedia({ audio: true, // video: true, }); const audioTrack = stream === null || stream === void 0 ? void 0 : stream.getAudioTracks()[0]; this.audioTrack = audioTrack; this.producer = await pTransport.produce({ track: audioTrack, }); } catch (error) { this.emit(CALL_EVENT.ERROR, { message: "Unable to access microphone", error: error }); } } } class IncommingCall extends Call { constructor(user, userToken, data) { super(user, userToken, data); this.isUnavailable = false; if (this.existingCall) { this.rejoin(data); } const callEvents = rtlayer_1.default.on(`call:${data.id}`, (data) => { var _a; data = JSON.parse(data); switch (data.type) { case 'call-ended': { super.hang(data); break; } case 'call-answered': { if (((_a = data.answeredBy) === null || _a === void 0 ? void 0 : _a.id) != user.id) { this.isUnavailable = true; this.setStatus(CALL_STATUS.ENDED); // Mark call as ended for this user this.emit(CALL_EVENT.UNAVAILABLE, data); } break; } default: { console.error("Invalid call type"); return; } } }); this.addEvent(callEvents); } rejoin(data) { this.isUnavailable = false; super.setupCall(); super.setStatus(CALL_STATUS.CONNECTED); setTimeout(() => { this.emit(CALL_EVENT.REJOINED, data); }, 100); } accept() { if (this.isUnavailable) return; if (this.existingCall) return; super.setupCall(); super.setStatus(CALL_STATUS.CONNECTED); } reject() { super.hang(); } } class OutgoingCall extends Call { constructor(user, userToken, data) { super(user, userToken, data); if (this.existingCall) { this.rejoin(data); } super.setupCall(); const callEvents = rtlayer_1.default.on(`call:${data.id}`, (data) => { data = JSON.parse(data); switch (data.type) { case 'call-ended': { super.hang(data); break; } case 'call-answered': { if (this.getStatus() == CALL_STATUS.CONNECTED) return; this.emit(CALL_EVENT.ANSWERED, data); this.setStatus(CALL_STATUS.CONNECTED); break; } default: { console.error("Invalid call type"); return; } } }); this.addEvent(callEvents); const botEvents = rtlayer_1.default.on(`user:${this.user.id}:call:${this.id}:bot`, (data) => { data = JSON.parse(data); if (data.type == "message") { this.emit(CALL_EVENT.MESSAGE, { message: data === null || data === void 0 ? void 0 : data.message, from: data === null || data === void 0 ? void 0 : data.from }); } }); this.addEvent(botEvents); } rejoin(data) { // Join the call without ringing // Hydrate call details by triggering rejoin event setTimeout(() => { this.emit(CALL_EVENT.REJOINED, data); super.setStatus(CALL_STATUS.CONNECTED); }, 100); } cancel() { super.hang(); } } let webrtc = new Map(); exports.default = (userToken, env = "prod") => { (0, util_1.setEnvironment)(env); if (webrtc.has(userToken)) return webrtc.get(userToken); webrtc.set(userToken, new WebRTC(userToken)); return webrtc.get(userToken); };