UNPKG

@openreplay/tracker-assist

Version:

Tracker plugin for screen assistance through the WebRTC

813 lines (812 loc) 37 kB
import { connect } from "socket.io-client"; import RequestLocalStream from "./LocalStream.js"; import { hasTag } from "./guards.js"; import RemoteControl, { RCStatus } from "./RemoteControl.js"; import CallWindow from "./CallWindow.js"; import AnnotationCanvas from "./AnnotationCanvas.js"; import ConfirmWindow from "./ConfirmWindow/ConfirmWindow.js"; import { callConfirmDefault } from "./ConfirmWindow/defaults.js"; import ScreenRecordingState from "./ScreenRecordingState.js"; import { pkgVersion } from "./version.js"; import Canvas from "./Canvas.js"; import { gzip } from "fflate"; var CallingState; (function (CallingState) { CallingState[CallingState["Requesting"] = 0] = "Requesting"; CallingState[CallingState["True"] = 1] = "True"; CallingState[CallingState["False"] = 2] = "False"; })(CallingState || (CallingState = {})); export default class Assist { constructor(app, options, noSecureMode = false) { this.app = app; this.noSecureMode = noSecureMode; this.version = pkgVersion; this.socket = null; this.calls = new Map(); this.canvasPeers = {}; this.canvasNodeCheckers = new Map(); this.assistDemandedRestart = false; this.callingState = CallingState.False; this.remoteControl = null; this.peerReconnectTimeout = null; this.agents = {}; this.canvasMap = new Map(); this.iceCandidatesBuffer = new Map(); this.setCallingState = (newState) => { this.callingState = newState; }; // @ts-ignore window.__OR_ASSIST_VERSION = this.version; this.options = Object.assign({ session_calling_peer_key: "__openreplay_calling_peer", session_control_peer_key: "__openreplay_control_peer", config: null, serverURL: null, onCallStart: () => { }, onAgentConnect: () => { }, onRemoteControlStart: () => { }, onDragCamera: () => { }, callConfirm: {}, controlConfirm: {}, // TODO: clear options passing/merging/overwriting recordingConfirm: {}, socketHost: "", compressionEnabled: false, compressionMinBatchSize: 5000, }, options); if (this.app.options.assistSocketHost) { this.options.socketHost = this.app.options.assistSocketHost; } if (document.hidden !== undefined) { const sendActivityState = () => this.emit("UPDATE_SESSION", { active: !document.hidden }); app.attachEventListener(document, "visibilitychange", sendActivityState, false, false); } const titleNode = document.querySelector("title"); const observer = titleNode && new MutationObserver(() => { this.emit("UPDATE_SESSION", { pageTitle: document.title }); }); app.addOnUxtCb((uxtId) => { this.emit("UPDATE_SESSION", { uxtId }); }); app.attachStartCallback(() => { if (this.assistDemandedRestart) { return; } this.onStart(); observer && observer.observe(titleNode, { subtree: true, characterData: true, childList: true, }); }); app.attachStopCallback(() => { if (this.assistDemandedRestart) { return; } this.clean(); observer && observer.disconnect(); }); app.attachCommitCallback((messages) => { if (this.agentsConnected) { const batchSize = messages.length; // @ts-ignore No need in statistics messages. TODO proper filter if (batchSize === 2 && // @ts-ignore No need in statistics messages. TODO proper filter messages[0]._id === 0 && // @ts-ignore No need in statistics messages. TODO proper filter messages[1]._id === 49) { return; } if (batchSize > this.options.compressionMinBatchSize && this.options.compressionEnabled) { const toSend = []; if (batchSize > 10000) { const middle = Math.floor(batchSize / 2); const firstHalf = messages.slice(0, middle); const secondHalf = messages.slice(middle); toSend.push(firstHalf); toSend.push(secondHalf); } else { toSend.push(messages); } toSend.forEach((batch) => { const str = JSON.stringify(batch); const byteArr = new TextEncoder().encode(str); gzip(byteArr, { mtime: 0 }, (err, result) => { if (err) { this.emit("messages", batch); } else { this.emit("messages_gz", result); } }); }); } else { this.emit("messages", messages); } } }); app.session.attachUpdateCallback((sessInfo) => this.emit("UPDATE_SESSION", sessInfo)); } emit(ev, args) { this.socket && this.socket.emit(ev, { meta: { tabId: this.app.getTabId() }, data: args, }); } get agentsConnected() { return Object.keys(this.agents).length > 0; } getHost() { if (this.options.socketHost) { return this.options.socketHost; } if (this.options.serverURL) { return new URL(this.options.serverURL).host; } return this.app.getHost(); } getBasePrefixUrl() { if (this.options.serverURL) { return new URL(this.options.serverURL).pathname; } return ""; } onStart() { var _a; const app = this.app; const sessionId = app.getSessionID(); // Common for all incoming call requests let callUI = null; let annot = null; // TODO: encapsulate let callConfirmWindow = null; let callConfirmAnswer = null; let callEndCallback = null; if (!sessionId) { return app.debug.error("No session ID"); } const peerID = `${app.getProjectKey()}-${sessionId}-${this.app.getTabId()}`; // SocketIO const socket = (this.socket = connect(this.getHost(), { path: this.getBasePrefixUrl() + "/ws-assist/socket", query: { peerId: peerID, identity: "session", tabId: this.app.getTabId(), sessionInfo: JSON.stringify(Object.assign({ uxtId: (_a = this.app.getUxtId()) !== null && _a !== void 0 ? _a : undefined, pageTitle: document.title, active: true, assistOnly: this.app.socketMode }, this.app.getSessionInfo())), }, extraHeaders: { sessionId, }, transports: ["websocket"], withCredentials: true, reconnection: true, reconnectionAttempts: 30, reconnectionDelay: 1000, reconnectionDelayMax: 25000, randomizationFactor: 0.5, })); socket.onAny((...args) => { if (args[0] === "messages" || args[0] === "UPDATE_SESSION") { return; } if (args[0] !== "webrtc_call_ice_candidate") { app.debug.log("Socket:", ...args); } socket.on("close", (e) => { app.debug.warn("Socket closed:", e); }); }); const onGrand = (id) => { var _a; if (!callUI) { callUI = new CallWindow(app.debug.error, this.options.callUITemplate); } if (this.remoteControl) { callUI === null || callUI === void 0 ? void 0 : callUI.showRemoteControl(this.remoteControl.releaseControl); } this.agents[id] = Object.assign(Object.assign({}, this.agents[id]), { onControlReleased: this.options.onRemoteControlStart((_a = this.agents[id]) === null || _a === void 0 ? void 0 : _a.agentInfo) }); this.emit("control_granted", id); annot = new AnnotationCanvas(); annot.mount(); return callingAgents.get(id); }; const onRelease = (id, isDenied) => { var _a, _b, _c; if (id) { const cb = this.agents[id].onControlReleased; delete this.agents[id].onControlReleased; typeof cb === "function" && cb(); this.emit("control_rejected", id); } if (annot != null) { annot.remove(); annot = null; } callUI === null || callUI === void 0 ? void 0 : callUI.hideRemoteControl(); if (this.callingState !== CallingState.True) { callUI === null || callUI === void 0 ? void 0 : callUI.remove(); callUI = null; } if (isDenied) { const info = id ? (_a = this.agents[id]) === null || _a === void 0 ? void 0 : _a.agentInfo : {}; (_c = (_b = this.options).onRemoteControlDeny) === null || _c === void 0 ? void 0 : _c.call(_b, info || {}); } }; this.remoteControl = new RemoteControl(this.options, onGrand, (id, isDenied) => onRelease(id, isDenied), (id) => this.emit("control_busy", id)); const onAcceptRecording = () => { socket.emit("recording_accepted"); }; const onRejectRecording = (agentData) => { var _a, _b; socket.emit("recording_rejected"); (_b = (_a = this.options).onRecordingDeny) === null || _b === void 0 ? void 0 : _b.call(_a, agentData || {}); }; const recordingState = new ScreenRecordingState(this.options.recordingConfirm); function processEvent(agentId, event, callback) { if (app.getTabId() === event.meta.tabId) { return callback === null || callback === void 0 ? void 0 : callback(agentId, event.data); } } if (this.remoteControl !== null) { socket.on("request_control", (agentId, dataObj) => { var _a; processEvent(agentId, dataObj, (_a = this.remoteControl) === null || _a === void 0 ? void 0 : _a.requestControl); }); socket.on("release_control", (agentId, dataObj) => { processEvent(agentId, dataObj, (_, data) => { var _a; return (_a = this.remoteControl) === null || _a === void 0 ? void 0 : _a.releaseControl(data); }); }); socket.on("scroll", (id, event) => { var _a; return processEvent(id, event, (_a = this.remoteControl) === null || _a === void 0 ? void 0 : _a.scroll); }); socket.on("click", (id, event) => { var _a; return processEvent(id, event, (_a = this.remoteControl) === null || _a === void 0 ? void 0 : _a.click); }); socket.on("move", (id, event) => { var _a; return processEvent(id, event, (_a = this.remoteControl) === null || _a === void 0 ? void 0 : _a.move); }); socket.on("startDrag", (id, event) => { var _a; return processEvent(id, event, (_a = this.remoteControl) === null || _a === void 0 ? void 0 : _a.startDrag); }); socket.on("drag", (id, event) => { var _a; return processEvent(id, event, (_a = this.remoteControl) === null || _a === void 0 ? void 0 : _a.drag); }); socket.on("stopDrag", (id, event) => { var _a; return processEvent(id, event, (_a = this.remoteControl) === null || _a === void 0 ? void 0 : _a.stopDrag); }); socket.on("focus", (id, event) => processEvent(id, event, (clientID, nodeID) => { const el = app.nodes.getNode(nodeID); if (el instanceof HTMLElement && this.remoteControl) { this.remoteControl.focus(clientID, el); } })); socket.on("input", (id, event) => { var _a; return processEvent(id, event, (_a = this.remoteControl) === null || _a === void 0 ? void 0 : _a.input); }); } // TODO: restrict by id socket.on("moveAnnotation", (id, event) => processEvent(id, event, (_, d) => annot && annot.move(d))); socket.on("startAnnotation", (id, event) => processEvent(id, event, (_, d) => annot === null || annot === void 0 ? void 0 : annot.start(d))); socket.on("stopAnnotation", (id, event) => processEvent(id, event, annot === null || annot === void 0 ? void 0 : annot.stop)); socket.on("WEBRTC_CONFIG", (config) => { if (config) { this.config = JSON.parse(config); } }); socket.on("NEW_AGENT", (id, info) => { var _a, _b; this.cleanCanvasConnections(); this.agents[id] = { onDisconnect: (_b = (_a = this.options).onAgentConnect) === null || _b === void 0 ? void 0 : _b.call(_a, info), agentInfo: info, // TODO ? }; if (this.app.active()) { this.assistDemandedRestart = true; this.app.stop(); this.app.clearBuffers(); this.app.waitStatus(0).then(() => { this.app.allowAppStart(); setTimeout(() => { this.app .start() .then(() => { this.assistDemandedRestart = false; }) .then(() => { var _a; (_a = this.remoteControl) === null || _a === void 0 ? void 0 : _a.reconnect([id]); }) .catch((e) => app.debug.error(e)); // TODO: check if it's needed; basically allowing some time for the app to finish everything before starting again }, 100); }); } }); socket.on("AGENTS_INFO_CONNECTED", (agentsInfo) => { this.cleanCanvasConnections(); agentsInfo.forEach((agentInfo) => { var _a, _b; if (!agentInfo.socketId) return; this.agents[agentInfo.socketId] = { agentInfo, onDisconnect: (_b = (_a = this.options).onAgentConnect) === null || _b === void 0 ? void 0 : _b.call(_a, agentInfo), }; }); if (this.app.active()) { this.assistDemandedRestart = true; this.app.stop(); this.app.clearBuffers(); this.app.waitStatus(0).then(() => { this.app.allowAppStart(); setTimeout(() => { this.app .start() .then(() => { this.assistDemandedRestart = false; }) .then(() => { var _a; (_a = this.remoteControl) === null || _a === void 0 ? void 0 : _a.reconnect(Object.keys(this.agents)); }) .catch((e) => app.debug.error(e)); }, 100); }); } }); socket.on("AGENT_DISCONNECTED", (id) => { var _a, _b, _c; (_a = this.remoteControl) === null || _a === void 0 ? void 0 : _a.releaseControl(); (_c = (_b = this.agents[id]) === null || _b === void 0 ? void 0 : _b.onDisconnect) === null || _c === void 0 ? void 0 : _c.call(_b); delete this.agents[id]; Object.values(this.calls).forEach((pc) => pc.close()); this.calls.clear(); recordingState.stopAgentRecording(id); endAgentCall({ socketId: id }); }); socket.on("NO_AGENT", () => { Object.values(this.agents).forEach((a) => { var _a; return (_a = a.onDisconnect) === null || _a === void 0 ? void 0 : _a.call(a); }); this.cleanCanvasConnections(); this.agents = {}; if (recordingState.isActive) recordingState.stopRecording(); }); socket.on("call_end", (socketId, msg) => { if (!callingAgents.has(socketId) || !msg) { app.debug.warn("Received call_end from unknown agent", socketId); return; } const { data: callId } = msg; endAgentCall({ socketId, callId }); }); socket.on("_agent_name", (id, info) => { if (app.getTabId() !== info.meta.tabId) return; const name = info.data; callingAgents.set(id, name); updateCallerNames(); }); socket.on("webrtc_canvas_answer", async (_, data) => { const pc = this.canvasPeers[data.id]; if (pc) { try { await pc.setRemoteDescription(new RTCSessionDescription(data.answer)); } catch (e) { app.debug.error("Error adding ICE candidate", e); } } }); socket.on("webrtc_canvas_ice_candidate", async (_, data) => { var _a; const pc = this.canvasPeers[data.id]; if (pc) { try { await pc.addIceCandidate(new RTCIceCandidate(data.candidate)); } catch (e) { app.debug.error("Error adding ICE candidate", e); } } else { this.iceCandidatesBuffer.set(data.id, ((_a = this.iceCandidatesBuffer .get(data.id)) === null || _a === void 0 ? void 0 : _a.concat([data.candidate])) || [data.candidate]); } }); // If a videofeed arrives, then we show the video in the ui socket.on("videofeed", (_, info) => { if (app.getTabId() !== info.meta.tabId) return; const feedState = info.data; callUI === null || callUI === void 0 ? void 0 : callUI.toggleVideoStream(feedState); }); socket.on("request_recording", (id, info) => { var _a, _b; if (app.getTabId() !== info.meta.tabId) return; const agentData = info.data; if (!recordingState.isActive) { (_b = (_a = this.options).onRecordingRequest) === null || _b === void 0 ? void 0 : _b.call(_a, JSON.parse(agentData)); recordingState.requestRecording(id, onAcceptRecording, () => onRejectRecording(agentData)); } else { this.emit("recording_busy"); } }); socket.on("stop_recording", (id, info) => { if (app.getTabId() !== info.meta.tabId) return; if (recordingState.isActive) { recordingState.stopAgentRecording(id); } }); socket.on("webrtc_call_offer", async (_, data) => { if (!this.calls.has(data.from)) { await handleIncomingCallOffer(data.from, data.offer); } }); socket.on("webrtc_call_ice_candidate", async (_, data) => { var _a; const pc = this.calls[data.from]; if (pc) { try { await pc.addIceCandidate(new RTCIceCandidate(data.candidate)); } catch (e) { app.debug.error("Error adding ICE candidate", e); } } else { this.iceCandidatesBuffer.set(data.from, ((_a = this.iceCandidatesBuffer .get(data.from)) === null || _a === void 0 ? void 0 : _a.concat([data.candidate])) || [data.candidate]); } }); const callingAgents = new Map(); // !! uses socket.io ID // TODO: merge peerId & socket.io id (simplest way - send peerId with the name) const lStreams = {}; function updateCallerNames() { callUI === null || callUI === void 0 ? void 0 : callUI.setAssistentName(callingAgents); } function endAgentCall({ socketId, callId, }) { callingAgents.delete(socketId); if (callingAgents.size === 0) { handleCallEnd(); } else { updateCallerNames(); if (callId) { handleCallEndWithAgent(callId); } } } const handleCallEndWithAgent = (id) => { var _a; (_a = this.calls.get(id)) === null || _a === void 0 ? void 0 : _a.close(); this.calls.delete(id); }; // call end handling const handleCallEnd = () => { var _a; Object.values(this.calls).forEach((pc) => pc.close()); this.calls.clear(); Object.values(lStreams).forEach((stream) => { stream.stop(); }); Object.keys(lStreams).forEach((peerId) => { delete lStreams[peerId]; }); // UI closeCallConfirmWindow(); if (((_a = this.remoteControl) === null || _a === void 0 ? void 0 : _a.status) === RCStatus.Disabled) { callUI === null || callUI === void 0 ? void 0 : callUI.remove(); annot === null || annot === void 0 ? void 0 : annot.remove(); callUI = null; annot = null; } else { callUI === null || callUI === void 0 ? void 0 : callUI.hideControls(); } this.emit("UPDATE_SESSION", { agentIds: [], isCallActive: false }); this.setCallingState(CallingState.False); sessionStorage.removeItem(this.options.session_calling_peer_key); callEndCallback === null || callEndCallback === void 0 ? void 0 : callEndCallback(); }; const closeCallConfirmWindow = () => { if (callConfirmWindow) { callConfirmWindow.remove(); callConfirmWindow = null; callConfirmAnswer = null; } }; const renegotiateConnection = async ({ pc, from, }) => { try { const offer = await pc.createOffer(); await pc.setLocalDescription(offer); this.emit("webrtc_call_offer", { from, offer }); } catch (error) { app.debug.error("Error with renegotiation:", error); } }; const handleIncomingCallOffer = async (from, offer) => { var _a, _b, _c, _d, _e, _f; app.debug.log("handleIncomingCallOffer", from); let confirmAnswer; const callingPeerIds = JSON.parse(sessionStorage.getItem(this.options.session_calling_peer_key) || "[]"); // if the caller is already in the list, then we immediately accept the call without ui if (callingPeerIds.includes(from) || this.callingState === CallingState.True) { confirmAnswer = Promise.resolve(true); } else { // set the state to wait for confirmation this.setCallingState(CallingState.Requesting); // call the call confirmation window confirmAnswer = requestCallConfirm(); // sound notification of a call this.playNotificationSound(); // after 30 seconds we drop the call setTimeout(() => { if (this.callingState !== CallingState.Requesting) { return; } initiateCallEnd(); }, 30000); } try { // waiting for a decision on accepting the challenge const agreed = await confirmAnswer; // if rejected, then terminate the call if (!agreed) { initiateCallEnd(); (_b = (_a = this.options).onCallDeny) === null || _b === void 0 ? void 0 : _b.call(_a); return; } // create a new RTCPeerConnection with ice server config const pc = new RTCPeerConnection({ iceServers: this.config, }); this.calls.set(from, pc); if (!callUI) { callUI = new CallWindow(app.debug.error, this.options.callUITemplate); callUI.setVideoToggleCallback((args) => { this.emit("videofeed", { streamId: from, enabled: args.enabled }); }); } // show buttons in the call window callUI.showControls(initiateCallEnd); if (!annot) { annot = new AnnotationCanvas(); annot.mount(); } // callUI.setLocalStreams(Object.values(lStreams)) try { // if there are no local streams in lStrems then we set if (!lStreams[from]) { app.debug.log("starting new stream for", from); // request a local stream, and set it to lStreams lStreams[from] = await RequestLocalStream(pc, renegotiateConnection.bind(null, { pc, from })); } // we pass the received tracks to Call ui callUI.setLocalStreams(Object.values(lStreams)); } catch (e) { app.debug.error("Error requesting local stream", e); // if something didn't work out, we terminate the call initiateCallEnd(); (_d = (_c = this.options).onCallDeny) === null || _d === void 0 ? void 0 : _d.call(_c); return; } // get all local tracks and add them to RTCPeerConnection // When we receive local ice candidates, we emit them via socket pc.onicecandidate = (event) => { if (event.candidate) { socket.emit("webrtc_call_ice_candidate", { from, candidate: event.candidate, }); } }; // when we get a remote stream, add it to call ui pc.ontrack = (event) => { const rStream = event.streams[0]; if (rStream && callUI) { callUI.addRemoteStream(rStream, from); const onInteraction = () => { callUI === null || callUI === void 0 ? void 0 : callUI.playRemote(); document.removeEventListener("click", onInteraction); }; document.addEventListener("click", onInteraction); } }; // set remote description on incoming request await pc.setRemoteDescription(new RTCSessionDescription(offer)); // create a response to the incoming request const answer = await pc.createAnswer(); // set answer as local description await pc.setLocalDescription(answer); // set the response as local socket.emit("webrtc_call_answer", { from, answer }); this.applyBufferedIceCandidates(from); // If the state changes to an error, we terminate the call // pc.onconnectionstatechange = () => { // if (pc.connectionState === 'disconnected' || pc.connectionState === 'failed') { // initiateCallEnd(); // } // }; // Update track when local video changes lStreams[from].onVideoTrack((vTrack) => { const sender = pc.getSenders().find((s) => { var _a; return ((_a = s.track) === null || _a === void 0 ? void 0 : _a.kind) === "video"; }); if (!sender) { app.debug.warn("No video sender found"); return; } sender.replaceTrack(vTrack); }); // if the user closed the tab or switched, then we end the call document.addEventListener("visibilitychange", () => { initiateCallEnd(); }); // when everything is set, we change the state to true this.setCallingState(CallingState.True); if (!callEndCallback) { callEndCallback = (_f = (_e = this.options).onCallStart) === null || _f === void 0 ? void 0 : _f.call(_e); } const callingPeerIdsNow = Array.from(this.calls.keys()); // in session storage we write down everyone with whom the call is established sessionStorage.setItem(this.options.session_calling_peer_key, JSON.stringify(callingPeerIdsNow)); this.emit("UPDATE_SESSION", { agentIds: callingPeerIdsNow, isCallActive: true, }); } catch (reason) { app.debug.log(reason); } }; // Functions for requesting confirmation, ending a call, notifying, etc. const requestCallConfirm = () => { if (callConfirmAnswer) { // If confirmation has already been requested return callConfirmAnswer; } callConfirmWindow = new ConfirmWindow(callConfirmDefault(this.options.callConfirm || { text: this.options.confirmText, style: this.options.confirmStyle, })); return (callConfirmAnswer = callConfirmWindow.mount().then((answer) => { closeCallConfirmWindow(); return answer; })); }; const initiateCallEnd = () => { this.emit("call_end"); handleCallEnd(); }; const startCanvasStream = async (stream, id) => { for (const agent of Object.values(this.agents)) { if (!agent.agentInfo) return; const uniqueId = `${agent.agentInfo.peerId}-${agent.agentInfo.id}-canvas-${id}`; if (!this.canvasPeers[uniqueId]) { this.canvasPeers[uniqueId] = new RTCPeerConnection({ iceServers: this.config, }); this.setupPeerListeners(uniqueId); this.applyBufferedIceCandidates(uniqueId); stream.getTracks().forEach((track) => { var _a; (_a = this.canvasPeers[uniqueId]) === null || _a === void 0 ? void 0 : _a.addTrack(track, stream); }); // Create SDP offer const offer = await this.canvasPeers[uniqueId].createOffer(); await this.canvasPeers[uniqueId].setLocalDescription(offer); // Send offer via signaling server socket.emit("webrtc_canvas_offer", { offer, id: uniqueId }); } } }; app.nodes.attachNodeCallback((node) => { const id = app.nodes.getID(node); if (id && hasTag(node, "canvas") && !app.sanitizer.isHidden(id)) { app.debug.log(`Creating stream for canvas ${id}`); const canvasHandler = new Canvas(node, id, 30, (stream) => { startCanvasStream(stream, id); }, app.debug.error); this.canvasMap.set(id, canvasHandler); if (this.canvasNodeCheckers.has(id)) { clearInterval(this.canvasNodeCheckers.get(id)); } const int = setInterval(() => { const isPresent = node.ownerDocument.defaultView && node.isConnected; if (!isPresent) { this.stopCanvasStream(id); clearInterval(int); } }, 5000); this.canvasNodeCheckers.set(id, int); } }); } setupPeerListeners(id) { const peer = this.canvasPeers[id]; if (!peer) return; // ICE candidates peer.onicecandidate = (event) => { if (event.candidate && this.socket) { this.socket.emit("webrtc_canvas_ice_candidate", { candidate: event.candidate, id, }); } }; } playNotificationSound() { if ("Audio" in window) { new Audio("https://static.openreplay.com/tracker-assist/notification.mp3") .play() .catch((e) => { this.app.debug.warn(e); }); } } // clear all data clean() { var _a; // sometimes means new agent connected, so we keep id for control (_a = this.remoteControl) === null || _a === void 0 ? void 0 : _a.releaseControl(false, true); if (this.peerReconnectTimeout) { clearTimeout(this.peerReconnectTimeout); this.peerReconnectTimeout = null; } this.cleanCanvasConnections(); Object.values(this.calls).forEach((pc) => pc.close()); this.calls.clear(); if (this.socket) { this.socket.disconnect(); this.app.debug.log("Socket disconnected"); } this.canvasMap.clear(); this.canvasPeers = {}; this.canvasNodeCheckers.forEach((int) => clearInterval(int)); this.canvasNodeCheckers.clear(); this.iceCandidatesBuffer.clear(); } cleanCanvasConnections() { var _a; Object.values(this.canvasPeers).forEach((pc) => pc === null || pc === void 0 ? void 0 : pc.close()); this.canvasPeers = {}; (_a = this.socket) === null || _a === void 0 ? void 0 : _a.emit("webrtc_canvas_restart"); } stopCanvasStream(id) { var _a, _b, _c; for (const agent of Object.values(this.agents)) { if (!agent.agentInfo) return; const uniqueId = `${agent.agentInfo.peerId}-${agent.agentInfo.id}-canvas-${id}`; (_a = this.socket) === null || _a === void 0 ? void 0 : _a.emit("webrtc_canvas_stop", { id: uniqueId }); if (this.canvasPeers[uniqueId]) { (_b = this.canvasPeers[uniqueId]) === null || _b === void 0 ? void 0 : _b.close(); delete this.canvasPeers[uniqueId]; (_c = this.canvasMap.get(id)) === null || _c === void 0 ? void 0 : _c.stop(); this.canvasMap.delete(id); this.canvasNodeCheckers.get(id) && clearInterval(this.canvasNodeCheckers.get(id)); this.canvasNodeCheckers.delete(id); } } } applyBufferedIceCandidates(from) { const buffer = this.iceCandidatesBuffer.get(from); if (buffer) { buffer.forEach((candidate) => { var _a; (_a = this.calls.get(from)) === null || _a === void 0 ? void 0 : _a.addIceCandidate(new RTCIceCandidate(candidate)); }); this.iceCandidatesBuffer.delete(from); } } } /** simple peers impl * const slPeer = new SLPeer({ initiator: true, stream: stream, }) * // slPeer.on('signal', (data: any) => { * // this.emit('c_signal', { data, id, }) * // }) * // this.socket?.on('c_signal', (tab: string, data: any) => { * // console.log(data) * // slPeer.signal(data) * // }) * // slPeer.on('error', console.error) * // this.emit('canvas_stream', { canvasId, }) * */