UNPKG

infobip-rtc

Version:

Infobip RTC JavaScript SDK - Infobip WebRTC API Implementation

297 lines 12.3 kB
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