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
JavaScript
"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);
};