UNPKG

@simplito/privmx-webendpoint

Version:

PrivMX Web Endpoint library

548 lines (547 loc) 24.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.WebRtcClient = void 0; const WebWorkerHelper_1 = require("./WebWorkerHelper"); const WebRtcConfig_1 = require("./WebRtcConfig"); const Types_1 = require("../Types"); const KeyStore_1 = require("./KeyStore"); const PeerConnectionsManager_1 = require("./PeerConnectionsManager"); const Logger_1 = require("./Logger"); const Queue_1 = require("./Queue"); const LocalAudioLevelMeter_1 = require("./audio/LocalAudioLevelMeter"); const ActiveSpeakerDetector_1 = require("./audio/ActiveSpeakerDetector"); const EventDispatcher_1 = require("../service/EventDispatcher"); const DataChannelCryptor_1 = require("./DataChannelCryptor"); class WebRtcClient { assetsDir; uniqId; e2eeWorker; webWorkerApi; configuration; keyStore = new KeyStore_1.KeyStore(); publishStreamHandle; // to moze byc uzyte kiedy wymagany jest update credentials (jak straca waznosc) peerCredentials; remoteStreamsListeners = new Map(); sequenceNumberByRemoteStreamId = new Map(); dataChannelByRemoteStreamId = new Map(); dataChannelCryptor; sequenceNumberOfSender; peerConnectionsManager; streamsApiInterface; activeSpeakerDetector; audioLevelCallback; // private mediaServerAvailPublishers: {[publisherId: number]: Publisher} = {}; encByReceiver = new WeakMap(); logger = new Logger_1.Logger(); peerConnectionReconfigureQueue; lastProcessedAnswer = {}; lastMeasuredLocalRMS = LocalAudioLevelMeter_1.LocalAudioLevelMeter.RMS_VALUE_OF_SILENCE; eventsDispatcher = new EventDispatcher_1.StateChangeDispatcher(); localAudioLevelMeters = new Map(); bootstrapDataChannel; constructor(assetsDir) { this.assetsDir = assetsDir; this.uniqId = "" + Math.random() + "-" + Math.random(); this.sequenceNumberOfSender = 1; this.peerConnectionsManager = new PeerConnectionsManager_1.PeerConnectionManager((roomId) => { return this.createPeerConnectionMultiForRoom(roomId, this.getPeerConnectionConfiguration()); }, (sessionId, candidate) => { return this.streamsApiInterface.trickle(sessionId, candidate); }); this.peerConnectionReconfigureQueue = new Queue_1.Queue(); this.peerConnectionReconfigureQueue.assignProcessorFunc(async (_item) => { if (_item.jsep && _item.jsep.type === "offer") { await this.reconfigureSingle(_item._room, _item.jsep); } else { await this.reconfigureSingleCreateOffer(_item._room); } }); this.activeSpeakerDetector = new ActiveSpeakerDetector_1.ActiveSpeakerDetector(ActiveSpeakerDetector_1.DEFAULTS); this.dataChannelCryptor = new DataChannelCryptor_1.DataChannelCryptor(this.getKeyStore()); } async ensureLocalAudioLevelMeter(track) { if (this.localAudioLevelMeters.has(track.id)) { return; } const worker = await this.getWorker(); const meter = new LocalAudioLevelMeter_1.LocalAudioLevelMeter(track, (onRms) => { const rmsToReport = track.enabled ? onRms : LocalAudioLevelMeter_1.LocalAudioLevelMeter.RMS_VALUE_OF_SILENCE; worker.postMessage({ operation: "rms", rms: rmsToReport }); this.lastMeasuredLocalRMS = onRms; }); this.localAudioLevelMeters.set(track.id, meter); try { await meter.init(this.assetsDir + "/rms-processor.js"); } catch (e) { this.localAudioLevelMeters.delete(track.id); meter.stop(); throw e; } } stopLocalAudioLevelMeter(track) { const meter = this.localAudioLevelMeters.get(track.id); if (!meter) { return; } this.localAudioLevelMeters.delete(track.id); meter.stop(); } setAudioLevelCallback(func) { this.audioLevelCallback = func; } bindApiInterface(streamsApiInterface) { this.streamsApiInterface = streamsApiInterface; } addRemoteStreamListener(listener) { let listeners = this.remoteStreamsListeners.get(listener.streamRoomId) || []; const exists = listeners.find((x) => x.streamId === listener.streamId); if (exists) { throw new Error("RemoteStreamListener with given params already exists."); } listeners.push(listener); this.remoteStreamsListeners.set(listener.streamRoomId, listeners); } getStreamStateChangeDispatcher() { return this.eventsDispatcher; } getConnectionManager() { if (!this.peerConnectionsManager) { throw new Error("No peerConnectionManager initialized."); } return this.peerConnectionsManager; } getWebRtcEventDispatcher() { return this.eventsDispatcher; } async getWorker() { if (!this.e2eeWorker) { const workerApi = await this.getWorkerApi(); this.e2eeWorker = workerApi.getWorker(); } if (!this.e2eeWorker) { throw new Error("Worker not initialized."); } return this.e2eeWorker; } async initPipeline(receiverTrackId, publisherId) { const worker = await this.getWorker(); const waitPromise = new Promise((resolve) => { const listener = (ev) => { if (ev.data.operation === "init-pipeline" && ev.data.id === receiverTrackId) { worker.removeEventListener("message", listener); resolve(); } }; worker.addEventListener("message", listener); worker.postMessage({ operation: "init-pipeline", id: receiverTrackId, publisherId: publisherId, }); }); return waitPromise; } async getWorkerApi() { if (!this.webWorkerApi) { this.webWorkerApi = new WebWorkerHelper_1.WebWorker(this.assetsDir, (frameInfo) => { if (this.audioLevelCallback && typeof this.audioLevelCallback === "function") { // report local rms to activeSpeakerDetector to have notifications for local streams this.activeSpeakerDetector.onFrame({ id: 0, rms: this.lastMeasuredLocalRMS, timestamp: Date.now(), }); const speakers = this.activeSpeakerDetector.onFrame({ id: frameInfo.publisherId, rms: frameInfo.rms, timestamp: Date.now(), }); // if (laudestParticipant === frameInfo.publisherId) { this.audioLevelCallback({ levels: speakers }); // } } }); await this.webWorkerApi.init_e2ee(); } return this.webWorkerApi; } getPeerConnectionConfiguration() { if (!this.configuration) { // throw new Error("No peerConnectionConfiguration created"); this.configuration = WebRtcConfig_1.WebRtcConfig.generateTurnConfiguration(this.peerCredentials); } return this.configuration; } async setTurnCredentials(turnCredentials) { this.peerCredentials = turnCredentials; } async createPeerConnectionWithLocalStream(streamHandle, streamRoomId, stream, dataTracks) { this.publishStreamHandle = streamHandle; this.configuration = WebRtcConfig_1.WebRtcConfig.generateTurnConfiguration(this.peerCredentials); const peerConnManager = this.getConnectionManager(); peerConnManager.initialize(streamRoomId, "publisher"); const pc = this.getConnectionManager().getConnectionWithSession(streamRoomId, "publisher").pc; if (stream.getTracks().length > 0) { const tracks = stream.getTracks(); this.e2eeWorker = await this.getWorker(); for (const track of tracks) { if (track.kind === "audio") { // add RMSProcessor await this.ensureLocalAudioLevelMeter(track); } const streamSender = pc.addTrack(track, stream); this.setupSenderTransform(streamSender); } } if (dataTracks) { for (const dataTrack of dataTracks) { const dataChannel = pc.createDataChannel("JanusDataChannel", { ordered: true, negotiated: false, }); dataTrack.dataChannelMeta.dataChannel = dataChannel; } } return pc; } removeSenderPeerConnectionOnUnpublish(streamRoomId, stream) { const peerConnManager = this.getConnectionManager(); const session = peerConnManager.getConnectionWithSession(streamRoomId, "publisher"); for (const track of stream.getAudioTracks()) { this.stopLocalAudioLevelMeter(track); } session.pc.close(); session.pc = undefined; } async updatePeerConnectionWithLocalStream(streamRoomId, localStream, tracksToAdd, tracksToRemove) { this.configuration = WebRtcConfig_1.WebRtcConfig.generateTurnConfiguration(this.peerCredentials); const peerConnManager = this.getConnectionManager(); peerConnManager.initialize(streamRoomId, "publisher"); const pc = this.getConnectionManager().getConnectionWithSession(streamRoomId, "publisher").pc; if (tracksToAdd.length > 0) { this.e2eeWorker = await this.getWorker(); for (const track of tracksToAdd) { if (track.kind === "audio") { await this.ensureLocalAudioLevelMeter(track); } const videoSender = pc.addTrack(track, localStream); if (window.RTCRtpScriptTransform) { const options = { operation: "encode", }; videoSender.transform = new RTCRtpScriptTransform(this.e2eeWorker, options); } else { const senderStreams = videoSender.createEncodedStreams(); this.e2eeWorker.postMessage({ operation: "encode", readableStream: senderStreams.readable, writableStream: senderStreams.writable, }, [senderStreams.readable, senderStreams.writable]); } } } if (tracksToRemove.length > 0) { const senders = pc.getSenders(); for (const oldTrack of tracksToRemove) { if (oldTrack.kind === "audio") { this.stopLocalAudioLevelMeter(oldTrack); } const sender = senders.find((s) => s.track === oldTrack); if (sender) { pc.removeTrack(sender); } } } return pc; } async encryptDataChannelData(data) { const nextSequenceNumber = ++this.sequenceNumberOfSender; return this.dataChannelCryptor.encryptToWireFormat({ plaintext: data, sequenceNumber: nextSequenceNumber, }); } createPeerConnectionMultiForRoom(roomId, configuration, _handle, _session) { const extConf = configuration; extConf.encodedInsertableStreams = true; const connection = new RTCPeerConnection(extConf); // gethering state change connection.addEventListener("icegatheringstatechange", (event) => { this.logger.debug("on ice state change: ", event); }); // ice candidate error connection.addEventListener("icecandidateerror", (event) => { this.logger.debug("on ice error: ", event); }); connection.addEventListener("connectionstatechange", (event) => { this.logger.debug("connectionstatechange: ", event); if (connection.connectionState === "connected") { this.logger.debug("Peers connected!"); } else { this.logger.debug("connection state: ", connection.connectionState); } this.eventsDispatcher.emit({ streamHandle: this.publishStreamHandle, state: connection.connectionState, }); }); connection.addEventListener("datachannel", (event) => { this.logger.debug("================ RECV datachannel: ", event.channel.id, event.channel.label); const dc = event.channel; dc.binaryType = "arraybuffer"; dc.onmessage = async (dataEvent) => { this.logger.debug("================ ON MESSAGE...."); const remoteStreamId = Number(event.channel.label); const frame = dataEvent.data instanceof Uint8Array ? dataEvent.data : dataEvent.data instanceof ArrayBuffer ? new Uint8Array(dataEvent.data) : new Uint8Array(dataEvent.data.buffer); try { const lastSeq = this.sequenceNumberByRemoteStreamId.get(remoteStreamId) || 0; const decrypted = await this.dataChannelCryptor.decryptFromWireFormat({ frame, lastSequenceNumber: lastSeq, }); this.sequenceNumberByRemoteStreamId.set(remoteStreamId, decrypted.seq); this.logger.debug("Calling listener for dataChannel with values: ", roomId, remoteStreamId, decrypted.data, Types_1.DataChannelCryptorDecryptStatus.OK); this.callRegisteredListenersForDataChannel(roomId, remoteStreamId, decrypted.data, Types_1.DataChannelCryptorDecryptStatus.OK); } catch (e) { if (e instanceof DataChannelCryptor_1.DataChannelCryptorError) { this.callRegisteredListenersForDataChannel(roomId, remoteStreamId, new Uint8Array(), e.code); } else { throw e; } } }; this.dataChannelByRemoteStreamId.set(Number(dc.label), dc); }); connection.addEventListener("iceconnectionstatechange", (event) => { this.logger.debug("iceconnectionstatechange: ", event); }); connection.addEventListener("negotiationneeded", async (_event) => { this.logger.debug("negotiationneeded: ", _event); // await this.startNegotiationMulti(roomId, (_event as any).target); }); connection.addEventListener("signalingstatechange", (event) => { this.logger.debug("signalingstatechange: ", event); }); connection.addEventListener("track", async (event) => { await this.addRemoteTrack(roomId, event); }); return connection; } async startNegotiationMulti(roomId, _rtcPeerConnection, _withIceRestart) { try { if (!this.peerConnectionReconfigureQueue) { throw new Error("ReconfigureQueue does not exist."); } this.peerConnectionReconfigureQueue.enqueue({ taskId: Math.floor(1 + Math.random() * 10000), _room: roomId, }); try { await this.peerConnectionReconfigureQueue.processAll(); } catch (e) { console.error("Error on onSubscriberAttached", e); } } catch (e) { console.error("Error on startNegotiationMulti", e); } } async updateKeys(_streamRoomId, keys) { this.logger.debug("=======> UPDATE KEYS", _streamRoomId, keys.length); this.keyStore.setKeys(keys); (await this.getWorkerApi()).setKeys(keys); } getKeyStore() { return this.keyStore; } setupSenderTransform(videoSender) { if (window.RTCRtpScriptTransform) { const options = { operation: "encode", }; videoSender.transform = new RTCRtpScriptTransform(this.e2eeWorker, options); } else { this.logger.debug("Worker - encoding frames using EncodedStreams"); const senderStreams = videoSender.createEncodedStreams(); this.e2eeWorker.postMessage({ operation: "encode", readableStream: senderStreams.readable, writableStream: senderStreams.writable, }, [senderStreams.readable, senderStreams.writable]); } } async setupReceiverTransform(receiver, publisherId, worker) { if ("RTCRtpScriptTransform" in window && !receiver.transform) { this.logger.debug("-> using RtpScriptTransform"); const id = receiver.track.id; receiver.transform = new window.RTCRtpScriptTransform(worker, { operation: "decode", id, publisherId, }); return; } this.logger.debug("-> using EncodedStreams"); // Fallback: Encoded Streams if (!this.encByReceiver.has(receiver) && "createEncodedStreams" in receiver && typeof receiver.createEncodedStreams === "function") { this.logger.debug("-> call for createEncodedStreams()"); const { readable, writable } = await receiver.createEncodedStreams(); const enc = { readable, writable, id: receiver.track.id, publisherId: publisherId, posted: false, }; this.encByReceiver.set(receiver, enc); this.logger.debug("-> posting EncodedStreams to worker (should happen only once)"); await this.initPipeline(enc.id, enc.publisherId); worker.postMessage({ operation: "decode", id: enc.id, publisherId: enc.publisherId, readableStream: enc.readable, writableStream: enc.writable, }, [enc.readable, enc.writable]); } else { this.logger.debug("-> EncodedStreams posted to worker already."); } } async waitUntilConnected(pc) { if (pc.iceConnectionState === "connected" || pc.iceConnectionState === "completed") return Promise.resolve(); return new Promise((resolve, reject) => { const onChange = () => { if (pc.iceConnectionState === "connected" || pc.iceConnectionState === "completed") { // pc.removeEventListener('iceconnectionstatechange', onChange); resolve(); } else if (pc.iceConnectionState === "failed" || pc.connectionState === "failed" || pc.connectionState === "closed") { pc.removeEventListener("iceconnectionstatechange", onChange); reject(new Error("ICE/DTLS not connected")); } }; pc.addEventListener("iceconnectionstatechange", onChange); }); } async teardownReceiver(receiver, worker) { const enc = this.encByReceiver.get(receiver); if (enc) { worker.postMessage({ operation: "stop", id: enc.id }); this.encByReceiver.delete(receiver); } } async addRemoteTrack(roomId, event) { const worker = await this.getWorker(); const track = event.track; const receiver = event.receiver; const publisherId = Number(event.streams[0].id); const peerConnection = this.getConnectionManager().getConnectionWithSession(roomId, "subscriber").pc; this.logger.debug("waitUntilConnected..."); await this.waitUntilConnected(peerConnection); this.logger.debug("setupReceiverTransform..."); await this.setupReceiverTransform(receiver, publisherId, worker); track.addEventListener("ended", async () => await this.teardownReceiver(receiver, worker)); this.callRegisteredListeners(roomId, event); } callRegisteredListeners(roomId, event) { const remoteStreamId = Number(event.streams[0].id); const listeners = this.remoteStreamsListeners.get(roomId); if (!listeners) { return; } const filteredListeners = listeners.filter((x) => x.streamId === remoteStreamId || x.streamId === undefined); for (const listener of filteredListeners) { if (listener.onRemoteStreamTrack && typeof listener.onRemoteStreamTrack === "function") { listener.onRemoteStreamTrack(event); } } } callRegisteredListenersForDataChannel(roomId, remoteStreamId, data, statusCode) { const listeners = this.remoteStreamsListeners.get(roomId); if (!listeners) { return; } const filteredListeners = listeners.filter((x) => x.streamId === remoteStreamId || x.streamId === undefined); for (const listener of filteredListeners) { if (listener.onRemoteData && typeof listener.onRemoteData === "function") { listener.onRemoteData(data, statusCode); } } } async onSubscriptionUpdated(_room, offer) { if (!this.peerConnectionReconfigureQueue) { throw new Error("ReconfigureQueue does not exist."); } this.peerConnectionReconfigureQueue.enqueue({ taskId: Math.floor(1 + Math.random() * 10000), _room, jsep: offer, }); try { await this.peerConnectionReconfigureQueue.processAll(); } catch (e) { console.error("Error on onSubscriberAttached", e); } } async onSubscriptionUpdatedSingle(_room, offer) { return this.reconfigureSingle(_room, offer); } async reconfigureSingle(room, offer) { if (!this.configuration) { throw new Error("Configuration missing."); } const janusConnection = this.getConnectionManager().getConnectionWithSession(room, "subscriber"); const peerConnection = janusConnection.pc; this.logger.debug("SUBSCRIBER RECV OFFER FROM PUBLISHER: ", offer.sdp); this.logger.debug("1. Setting up remoteDescription..."); if (!this.bootstrapDataChannel) { const bootstrap = peerConnection.createDataChannel("JanusDataChannel"); bootstrap.onerror = (e) => { console.error(e); throw new Error("Cannot initialize Bootrstrap dataChannel"); }; } await peerConnection.setRemoteDescription(new RTCSessionDescription({ type: offer.type, sdp: offer.sdp })); this.logger.debug("offer from Janus: ", JSON.stringify(offer, null, 2)); this.logger.debug("2. Creating an answer...", "peerConnection state", peerConnection.connectionState); const answer = await peerConnection.createAnswer(); this.logger.debug("3. Setting up localDescription..."); await peerConnection.setLocalDescription(new RTCSessionDescription(answer)); // this.subscriberAttachedProcessing = false this.lastProcessedAnswer[room] = answer; return answer; } async reconfigureSingleCreateOffer(room) { if (!this.configuration) { throw new Error("Configuration missing."); } const janusConnection = this.getConnectionManager().getConnectionWithSession(room, "publisher"); const peerConnection = janusConnection.pc; const offer = await peerConnection.createOffer(); await peerConnection.setLocalDescription(new RTCSessionDescription({ type: "offer", sdp: offer.sdp })); return offer; } } exports.WebRtcClient = WebRtcClient;