infobip-rtc
Version:
Infobip RTC JavaScript SDK - Infobip WebRTC API Implementation
297 lines • 12.3 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { EventEmitter } from "events";
import { v4 as uuid } from "uuid";
import { CallsApiEvent } from "../event/CallsApiEvents";
import { WsAction } from "../ws/WsAction";
import { DataChannelEvent } from "../event/DataChannelEvents";
import { ApplicationErrorCode } from "../ApplicationErrorCode";
import { AsyncLock } from "./util/AsyncLock";
const MAX_MSGS_PER_SEC = 50;
const BUFFER_THRESHOLD = 8192;
export class DefaultDataChannel {
constructor(gateway, logger, callId, currentUserIdentity, participantResolver, iceCandidateHandler, canSendMessage, apiEventEmitter, conferenceId) {
this.gateway = gateway;
this.logger = logger;
this.callId = callId;
this.currentUserIdentity = currentUserIdentity;
this.participantResolver = participantResolver;
this.iceCandidateHandler = iceCandidateHandler;
this.canSendMessage = canSendMessage;
this.apiEventEmitter = apiEventEmitter;
this.conferenceId = conferenceId;
this.dataEventEmitter = new EventEmitter();
this.textRoomParticipants = {};
this.availabilityLock = new AsyncLock();
this.timeLock = new AsyncLock();
this.bufferLock = new AsyncLock();
this.pendingMessages = {};
this.setupAvailabilityLock();
}
setupAvailabilityLock() {
this.availabilityLock.acquireLock().then(lock => {
const oldLock = this.currentAvailabilityLock;
this.currentAvailabilityLock = lock;
oldLock === null || oldLock === void 0 ? void 0 : oldLock.release();
});
}
initialize(event, rtcConfig) {
this.textRoomId = event.id;
if (!this.dataChannelPc) {
this.dataChannelPc = new RTCPeerConnection(rtcConfig);
this.dataChannelPc.ondatachannel = event => this.onDataChannel(event);
this.dataChannelPc.onicecandidate = ev => this.iceCandidateHandler(ev);
this.dataChannel = this.dataChannelPc.createDataChannel(this.conferenceId);
this.initializeDataChannelListeners();
}
else {
this.dataChannelPc.restartIce();
}
this.sendAnswer(event.description);
}
on(name, handler) {
if (!Object.values(DataChannelEvent)
.find(apiEvent => apiEvent === name)) {
throw new Error(`Unknown event: ${name}!`);
}
this.dataEventEmitter.on(name, handler);
}
send(text, to) {
return __awaiter(this, void 0, void 0, function* () {
if (!this.canSendMessage()) {
this.logger.warn("Data channel is unavailable until a call is added to a conference or a dialog!");
throw {
id: '10103',
name: 'MEDIA_ERROR',
description: 'Data channel is unavailable until a call is added to a conference or a dialog'
};
}
return yield this.availabilityLock.withLock(() => __awaiter(this, void 0, void 0, function* () {
var _a;
if (!this.dataChannel || this.dataChannel.readyState !== 'open') {
this.logger.warn("Data channel is unavailable!");
throw {
id: '10103',
name: 'MEDIA_ERROR',
description: 'Data channel is unavailable'
};
}
if (((_a = this.dataChannel) === null || _a === void 0 ? void 0 : _a.bufferedAmount) >= BUFFER_THRESHOLD) {
this.currentBufferLock = yield this.bufferLock.acquireLock();
}
const currentTimeLock = yield this.timeLock.acquireLock();
setTimeout(() => currentTimeLock.release(), 1000 / MAX_MSGS_PER_SEC);
let transactionId = uuid();
let recipient = this.getMessageRecipient(to);
this.sendTextMessage(transactionId, recipient, text);
return transactionId;
}));
});
}
destroy() {
var _a, _b;
(_a = this.dataChannel) === null || _a === void 0 ? void 0 : _a.close();
(_b = this.dataChannelPc) === null || _b === void 0 ? void 0 : _b.close();
this.dataChannelPc = null;
this.dataChannel = null;
}
removeParticipantOnReconnect(identity) {
delete this.textRoomParticipants[identity];
}
addParticipantOnReconnect(identity) {
this.textRoomParticipants[identity] = this.participantResolver(identity);
}
getMessageRecipient(to) {
if (!to) {
return null;
}
const identifier = to.identifier;
if (!this.participantResolver(identifier)) {
this.logger.warn(`Unknown recipient! ${identifier}`, this.callId);
throw {
id: '10103',
name: 'MEDIA_ERROR',
description: 'Recipient cannot be found'
};
}
if (!this.textRoomParticipants[identifier]) {
this.logger.warn(`Recipient data channel is unavailable! ${identifier}`, this.callId);
throw {
id: '10103',
name: 'MEDIA_ERROR',
description: 'Recipient\'s data channel is unavailable'
};
}
return identifier;
}
onDataChannel(event) {
const dataChannel = event.channel;
dataChannel.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.textroom === "join") {
this.handleJoinMessage(data);
}
else if (data.textroom === "success") {
this.handleSuccessMessage(data);
}
else if (data.textroom === "message") {
this.handleDataMessage(data);
}
else if (data.textroom === "announcement") {
this.handleAnnouncementData(data);
}
else if (data.textroom === "leave") {
this.handleLeaveMessage(data);
}
else if (data.textroom === "error") {
this.handleErrorMessage(data);
}
};
}
handleErrorMessage(data) {
const transactionId = data.transaction;
if (this.pendingMessages[transactionId]) {
this.emitDataSent(transactionId, false);
delete this.pendingMessages[transactionId];
}
else {
this.logger.error(`Received TextRoom Plugin error from Janus: ${JSON.stringify(data)}`, this.callId);
this.apiEventEmitter.emit(CallsApiEvent.ERROR, {
errorCode: {
name: "EC_INTERNAL_SERVER_ERROR",
description: "Internal Server Error."
}
});
}
}
handleDataMessage(data) {
const text = data.text;
const isDirect = !!data.whisper;
const from = this.textRoomParticipants[data.from];
if (data.from !== this.currentUserIdentity) {
this.emitDataReceived(text, from.endpoint, isDirect, new Date(data.date));
}
}
handleAnnouncementData({ text, date }) {
this.emitBroadcastTextReceived(text, new Date(date));
}
emitDataSent(id, delivered) {
this.dataEventEmitter.emit(DataChannelEvent.TEXT_DELIVERED_EVENT, {
id: id,
date: new Date(),
delivered: delivered
});
}
emitDataReceived(text, from, isDirect, date) {
this.dataEventEmitter.emit(DataChannelEvent.TEXT_RECEIVED_EVENT, {
text: text,
isDirect: isDirect,
from: from,
date: date
});
}
emitBroadcastTextReceived(text, date) {
this.dataEventEmitter.emit(DataChannelEvent.BROADCAST_TEXT_RECEIVED_EVENT, {
text: text,
date: date
});
}
handleLeaveMessage(data) {
let username = data.username;
delete this.textRoomParticipants[username];
}
handleSuccessMessage(data) {
var _a;
let participants = data.participants;
if (participants) {
for (const participant of participants) {
let username = participant.username;
this.textRoomParticipants[username] = this.participantResolver(username);
}
(_a = this.currentAvailabilityLock) === null || _a === void 0 ? void 0 : _a.release();
}
let sent = data.sent;
if (sent) {
Object.keys(sent).forEach(identity => {
const success = sent[identity];
this.emitDataSent(data.transaction, success);
});
}
else {
this.emitDataSent(data.transaction, true);
}
}
handleJoinMessage(data) {
const username = data.username;
this.textRoomParticipants[username] = this.participantResolver(username);
}
initializeDataChannelListeners() {
this.dataChannel.onopen = () => this.sendJoinMessage();
this.dataChannel.onerror = (errorEvent) => {
var _a;
if (errorEvent.error.errorDetail === "data-channel-failure") {
this.logger.warn(`Data channel failure! ${(_a = errorEvent === null || errorEvent === void 0 ? void 0 : errorEvent.error) === null || _a === void 0 ? void 0 : _a.message}`);
this.apiEventEmitter.emit(CallsApiEvent.ERROR, { errorCode: ApplicationErrorCode.MEDIA_ERROR });
}
};
this.dataChannel.bufferedAmountLowThreshold = BUFFER_THRESHOLD;
this.dataChannel.onbufferedamountlow = () => {
var _a;
(_a = this.currentBufferLock) === null || _a === void 0 ? void 0 : _a.release();
this.currentBufferLock = null;
};
}
;
sendAnswer(description) {
return __awaiter(this, void 0, void 0, function* () {
yield this.dataChannelPc.setRemoteDescription(description);
const answer = yield this.dataChannelPc.createAnswer();
yield this.dataChannelPc.setLocalDescription(answer);
this.gateway.send({
action: WsAction.SETUP_ACK,
description: answer
});
});
}
sendJoinMessage() {
if (!this.dataChannel || this.dataChannel.readyState !== 'open') {
this.logger.warn('Data channel is not open!', this.callId);
return;
}
const janusMessage = {
textroom: "join",
transaction: uuid(),
room: this.textRoomId,
username: this.currentUserIdentity
};
this.sendDataChannelMessage(janusMessage);
}
sendTextMessage(transactionId, recipient, text) {
this.pendingMessages[transactionId] = recipient;
const janusMessage = {
textroom: "message",
transaction: transactionId,
room: this.textRoomId,
to: recipient || undefined,
text: text
};
this.sendDataChannelMessage(janusMessage);
}
sendDataChannelMessage(janusMessage) {
try {
this.dataChannel.send(JSON.stringify(janusMessage));
}
catch (error) {
this.logger.warn(`Failed sending message through data channel! ${error === null || error === void 0 ? void 0 : error.code} ${error === null || error === void 0 ? void 0 : error.message}`, this.callId);
throw ApplicationErrorCode.MEDIA_ERROR;
}
}
}
//# sourceMappingURL=DefaultDataChannel.js.map