@openreplay/tracker-assist
Version:
Tracker plugin for screen assistance through the WebRTC
872 lines (871 loc) • 39.7 kB
JavaScript
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) {
var _a;
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.tabBus = null;
this.tabState = {
rcActive: undefined,
isCallActive: false,
agentIds: [],
};
this.setCallingState = (newState) => {
this.callingState = newState;
};
this.callUI = null;
this.annot = null;
this.publishState = (state) => {
var _a;
Object.assign(this.tabState, state);
(_a = this.tabBus) === null || _a === void 0 ? void 0 : _a.postMessage(state);
};
this.handleTabStateMessage = (msg) => {
var _a, _b, _c, _d, _e;
if (!msg)
return;
if (msg.data.type === "assist_state_check") {
if (this.tabState.isCallActive || this.tabState.rcActive) {
this.publishState(Object.assign({ type: "assist_state" }, this.tabState));
}
}
if (msg.data.type === "assist_state") {
if (msg.data.update === "call") {
if (msg.data.isCallActive) {
if (!this.callUI) {
this.callUI = new CallWindow(this.app.debug.error, this.options.callUITemplate);
}
const initiateCallEnd = () => {
this.emit("call_end");
};
this.callUI.showControls(initiateCallEnd);
if (!this.annot) {
this.annot = new AnnotationCanvas();
this.annot.mount();
}
}
else {
(_a = this.annot) === null || _a === void 0 ? void 0 : _a.remove();
this.annot = null;
(_b = this.callUI) === null || _b === void 0 ? void 0 : _b.hideControls();
(_c = this.callUI) === null || _c === void 0 ? void 0 : _c.remove();
this.callUI = null;
}
}
if (msg.data.update === "rc") {
if (msg.data.rcActive) {
(_d = this.remoteControl) === null || _d === void 0 ? void 0 : _d.grantControl(msg.data.rcActive, true);
}
else {
(_e = this.remoteControl) === null || _e === void 0 ? void 0 : _e.releaseControl(false, false, true);
}
}
Object.assign(this.tabState, msg.data);
}
};
// @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);
}
this.tabBus =
"BroadcastChannel" in window ? new BroadcastChannel("or-assist") : null;
(_a = this.tabBus) === null || _a === void 0 ? void 0 : _a.addEventListener("message", this.handleTabStateMessage);
const titleNode = document.querySelector("title");
const observer = titleNode &&
new MutationObserver(() => {
this.emit("UPDATE_SESSION", { pageTitle: document.title });
});
app.attachStartCallback(() => {
if (this.assistDemandedRestart) {
return;
}
this.onStart();
this.publishState({ type: "assist_state_check" });
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() {
const app = this.app;
const sessionId = app.getSessionID();
// 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({ 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, _b;
if (!this.callUI) {
this.callUI = new CallWindow(app.debug.error, this.options.callUITemplate);
}
if (this.remoteControl) {
(_a = this.callUI) === null || _a === void 0 ? void 0 : _a.showRemoteControl(this.remoteControl.releaseControl);
}
this.agents[id] = Object.assign(Object.assign({}, this.agents[id]), { onControlReleased: this.options.onRemoteControlStart((_b = this.agents[id]) === null || _b === void 0 ? void 0 : _b.agentInfo) });
this.emit("control_granted", id);
this.annot = new AnnotationCanvas();
this.annot.mount();
return callingAgents.get(id);
};
const onRelease = (id, isDenied) => {
var _a, _b, _c, _d, _e;
if (id) {
const cb = this.agents[id].onControlReleased;
delete this.agents[id].onControlReleased;
typeof cb === "function" && cb();
this.emit("control_rejected", id);
}
if (this.annot != null) {
this.annot.remove();
this.annot = null;
}
(_a = this.callUI) === null || _a === void 0 ? void 0 : _a.hideRemoteControl();
if (this.callingState !== CallingState.True) {
(_b = this.callUI) === null || _b === void 0 ? void 0 : _b.remove();
this.callUI = null;
}
if (isDenied) {
const info = id ? (_c = this.agents[id]) === null || _c === void 0 ? void 0 : _c.agentInfo : {};
(_e = (_d = this.options).onRemoteControlDeny) === null || _e === void 0 ? void 0 : _e.call(_d, info || {});
}
};
this.remoteControl = new RemoteControl(this.options, onGrand, onRelease, (id) => this.emit("control_busy", id), (activeId) => this.publishState({
type: "assist_state",
update: "rc",
rcActive: activeId,
}));
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) => this.annot && this.annot.move(d)));
socket.on("startAnnotation", (id, event) => processEvent(id, event, (_, d) => { var _a; return (_a = this.annot) === null || _a === void 0 ? void 0 : _a.start(d); }));
socket.on("stopAnnotation", (id, event) => { var _a; return processEvent(id, event, (_a = this.annot) === null || _a === void 0 ? void 0 : _a.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) => {
var _a;
if (app.getTabId() !== info.meta.tabId)
return;
const feedState = info.data;
(_a = this.callUI) === null || _a === void 0 ? void 0 : _a.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 = {};
const updateCallerNames = () => {
var _a;
(_a = this.callUI) === null || _a === void 0 ? void 0 : _a.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, _b, _c, _d;
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) {
(_b = this.callUI) === null || _b === void 0 ? void 0 : _b.remove();
(_c = this.annot) === null || _c === void 0 ? void 0 : _c.remove();
this.callUI = null;
this.annot = null;
}
else {
(_d = this.callUI) === null || _d === void 0 ? void 0 : _d.hideControls();
}
this.emit("UPDATE_SESSION", { agentIds: [], isCallActive: false });
this.setCallingState(CallingState.False);
sessionStorage.removeItem(this.options.session_calling_peer_key);
this.publishState({
type: "assist_state",
update: "call",
isCallActive: false,
agentIds: [],
});
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 (!this.callUI) {
this.callUI = new CallWindow(app.debug.error, this.options.callUITemplate);
this.callUI.setVideoToggleCallback((args) => {
this.emit("videofeed", { streamId: from, enabled: args.enabled });
});
}
// show buttons in the call window
this.callUI.showControls(initiateCallEnd);
if (!this.annot) {
this.annot = new AnnotationCanvas();
this.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
this.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 && this.callUI) {
this.callUI.addRemoteStream(rStream, from);
const onInteraction = () => {
var _a;
(_a = this.callUI) === null || _a === void 0 ? void 0 : _a.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);
});
// 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,
});
this.publishState({
type: "assist_state",
update: "call",
isCallActive: true,
agentIds: callingPeerIdsNow,
});
}
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);
}
}
}