From 3b85eb2f3e50b4cdf7e92818d568d94ea2b025e8 Mon Sep 17 00:00:00 2001 From: ShiKhu Date: Wed, 30 Jun 2021 22:52:42 +0200 Subject: [PATCH] wip (tracker-assist & frontend): disconnections correct handling & plugin UI --- .../MessageDistributor/MessageDistributor.ts | 92 +------- .../StatedScreen/Screen/BaseScreen.ts | 5 +- .../StatedScreen/StatedScreen.ts | 10 +- .../managers/AssistManager.ts | 154 +++++++++++++ frontend/app/player/singletone.js | 2 +- tracker/tracker-assist/package-lock.json | 5 + tracker/tracker-assist/package.json | 1 + tracker/tracker-assist/src/CallWindow.ts | 210 +++++++++++++++--- tracker/tracker-assist/src/Mouse.ts | 4 +- tracker/tracker-assist/src/confirm.ts | 76 +++++++ tracker/tracker-assist/src/index.ts | 103 +++++---- 11 files changed, 489 insertions(+), 173 deletions(-) create mode 100644 frontend/app/player/MessageDistributor/managers/AssistManager.ts create mode 100644 tracker/tracker-assist/src/confirm.ts diff --git a/frontend/app/player/MessageDistributor/MessageDistributor.ts b/frontend/app/player/MessageDistributor/MessageDistributor.ts index 54eb5b878..fe95b9291 100644 --- a/frontend/app/player/MessageDistributor/MessageDistributor.ts +++ b/frontend/app/player/MessageDistributor/MessageDistributor.ts @@ -23,13 +23,12 @@ import MouseManager from './managers/MouseManager'; import PerformanceTrackManager from './managers/PerformanceTrackManager'; import WindowNodeCounter from './managers/WindowNodeCounter'; import ActivityManager from './managers/ActivityManager'; +import AssistManager from './managers/AssistManager'; import MessageReader from './MessageReader'; -import { ID_TP_MAP } from './messages'; import { INITIAL_STATE as PARENT_INITIAL_STATE } from './StatedScreen'; -import type Peer from 'peerjs'; import type { TimedMessage } from './Timed'; const LIST_NAMES = [ "redux", "mobx", "vuex", "ngrx", "graphql", "exceptions", "profiles", "longtasks" ] as const; @@ -89,6 +88,7 @@ export default class MessageDistributor extends StatedScreen { private readonly resizeManager: ListWalker = new ListWalker([]); private readonly pagesManager: PagesManager; private readonly mouseManager: MouseManager; + private readonly assistManager: AssistManager; private readonly scrollManager: ListWalker = new ListWalker(); @@ -105,6 +105,7 @@ export default class MessageDistributor extends StatedScreen { super(); this.pagesManager = new PagesManager(this, this.session.isMobile) this.mouseManager = new MouseManager(this); + this.assistManager = new AssistManager(session, this); this.sessionStart = this.session.startedAt; @@ -112,7 +113,7 @@ export default class MessageDistributor extends StatedScreen { // const sockUrl = `wss://live.openreplay.com/1/${ this.session.siteId }/${ this.session.sessionId }/${ jwt }`; // this.subscribeOnMessages(sockUrl); initListsDepr({}) - this.connectToPeer(); + this.assistManager.connect(); } else { this.activirtManager = new ActivityManager(this.session.duration.milliseconds); /* == REFACTOR_ME == */ @@ -135,90 +136,10 @@ export default class MessageDistributor extends StatedScreen { this.lists.exceptions.add(e); }); /* === */ - this._loadMessages(); + this.loadMessages(); } } - private getPeerID(): string { - return `${this.session.projectKey}-${this.session.sessionId}` - } - - private peer: Peer | null = null; - private connectToPeer() { - this.setMessagesLoading(true); - import('peerjs').then(({ default: Peer }) => { - // @ts-ignore - console.log(new URL(window.ENV.API_EDP).host) - const peer = new Peer({ - // @ts-ignore - host: new URL(window.ENV.API_EDP).host, - path: '/assist', - port: 80, - }); - this.peer = peer; - peer.on("open", me => { - console.log("peer opened", me); - const id = this.getPeerID(); - console.log("trying to connect to", id) - const conn = peer.connect(id); - console.log("Peer ", peer) - - conn.on('open', () => { - this.setMessagesLoading(false); - let i = 0; - console.log("peer connected") - conn.on('data', (data) => { - if (!Array.isArray(data)) { return; } - let time = 0; - let ts0 = 0; - (data as Array).forEach(msg => { - msg.tp = ID_TP_MAP[msg._id]; // _id goes from tracker - if (msg.tp === "timestamp") { - ts0 = ts0 || msg.timestamp - time = msg.timestamp - ts0; - return; - } - const tMsg: TimedMessage = Object.assign(msg, { - time, - _index: i, - }); - this.distributeMessage(tMsg, i++); - }); - }); - }); - }); - }); - } - - callPeer(localStream: MediaStream, onStream: (s: MediaStream)=>void, onClose: () => void, onRefuse?: ()=> void): ()=>void { - if (!this.peer) { return Function; } - const conn = this.peer.connections[this.getPeerID()]?.[0]; - if (!conn || !conn.open) { return Function; } // Conn not established - const call = this.peer.call(conn.peer, localStream); - console.log('calling...') - // on refuse? - call.on('stream', onStream); - call.on("close", onClose); - call.on("error", onClose) - - return () => call.close(); - } - - requestMouse(): ()=>void { - if (!this.peer) { return Function; } - const conn = this.peer.connections[this.getPeerID()]?.[0]; - if (!conn || !conn.open) { return Function; } - const onMouseMove = (e) => { - // @ts-ignore - const data = this._getInternalCoordinates(e) - conn.send({ x: Math.round(data.x), y: Math.round(data.y) }); // debounce? - } - //@ts-ignore - this.overlay.addEventListener("mousemove", onMouseMove); - //@ts-ignore - return () => this.overlay.removeEventListener("mousemove", onMouseMove); - } - // subscribeOnMessages(sockUrl) { // this.setMessagesLoading(true); @@ -240,7 +161,7 @@ export default class MessageDistributor extends StatedScreen { // this._socket = socket; // } - _loadMessages(): void { + private loadMessages(): void { const fileUrl: string = this.session.mobsUrl; this.setMessagesLoading(true); window.fetch(fileUrl) @@ -545,5 +466,6 @@ export default class MessageDistributor extends StatedScreen { super.clean(); //if (this._socket) this._socket.close(); update(INITIAL_STATE); + this.assistManager.clear(); } } \ No newline at end of file diff --git a/frontend/app/player/MessageDistributor/StatedScreen/Screen/BaseScreen.ts b/frontend/app/player/MessageDistributor/StatedScreen/Screen/BaseScreen.ts index 6801e0c42..cdfd72d14 100644 --- a/frontend/app/player/MessageDistributor/StatedScreen/Screen/BaseScreen.ts +++ b/frontend/app/player/MessageDistributor/StatedScreen/Screen/BaseScreen.ts @@ -1,6 +1,3 @@ -import Marker from './Marker'; -import Cursor from './Cursor'; -import Inspector from './Inspector'; import styles from './screen.css'; import { getState } from '../../../store'; @@ -15,7 +12,7 @@ export const INITIAL_STATE: { } -export default class BaseScreen { +export default abstract class BaseScreen { private readonly iframe: HTMLIFrameElement; public readonly overlay: HTMLDivElement; private readonly _screen: HTMLDivElement; diff --git a/frontend/app/player/MessageDistributor/StatedScreen/StatedScreen.ts b/frontend/app/player/MessageDistributor/StatedScreen/StatedScreen.ts index ab734d7b7..61fce0532 100644 --- a/frontend/app/player/MessageDistributor/StatedScreen/StatedScreen.ts +++ b/frontend/app/player/MessageDistributor/StatedScreen/StatedScreen.ts @@ -11,33 +11,33 @@ export const INITIAL_STATE = { export default class StatedScreen extends Screen { - setMessagesLoading(messagesLoading) { + setMessagesLoading(messagesLoading: boolean) { // @ts-ignore this.display(!messagesLoading); update({ messagesLoading }); } - setCSSLoading(cssLoading) { + setCSSLoading(cssLoading: boolean) { // @ts-ignore this.displayFrame(!cssLoading); update({ cssLoading }); } - setDisconnected(disconnected) { + setDisconnected(disconnected: boolean) { if (!getState().live) return; //? // @ts-ignore this.display(!disconnected); update({ disconnected }); } - setUserPageLoading(userPageLoading) { + setUserPageLoading(userPageLoading: boolean) { // @ts-ignore this.display(!userPageLoading); update({ userPageLoading }); } - setSize({ height, width }) { + setSize({ height, width }: { height: number, width: number }) { update({ width, height }); // @ts-ignore this.scale(); diff --git a/frontend/app/player/MessageDistributor/managers/AssistManager.ts b/frontend/app/player/MessageDistributor/managers/AssistManager.ts new file mode 100644 index 000000000..2c401e298 --- /dev/null +++ b/frontend/app/player/MessageDistributor/managers/AssistManager.ts @@ -0,0 +1,154 @@ +import type Peer from 'peerjs'; +import type { DataConnection, MediaConnection } from 'peerjs'; +import type MessageDistributor from '../MessageDistributor'; +import type { TimedMessage } from '../Timed'; +import type { Message } from '../messages' +import { ID_TP_MAP } from '../messages'; + + + +export default class AssistManager { + constructor(private session, private md: MessageDistributor) {} + + private get peerID(): string { + return `${this.session.projectKey}-${this.session.sessionId}` + } + + private peer: Peer | null = null; + connect() { + if (this.peer != null) { + console.error("AssistManager: trying to connect more than once"); + return; + } + this.md.setMessagesLoading(true); + import('peerjs').then(({ default: Peer }) => { + // @ts-ignore + const peer = new Peer({ + // @ts-ignore + host: new URL(window.ENV.API_EDP).host, + path: '/assist', + port: 80, + }); + this.peer = peer; + this.peer.on('error', e => { + if (e.type === 'peer-unavailable') { + this.connectToPeer(); // TODO: MAX_ATTEMPT_TIME + } else { + console.error(`PeerJS error (on peer). Type ${e.type}`, e); + } + }) + peer.on("open", me => { + console.log("peer opened", me); + this.connectToPeer(); + }); + }); + } + + private connectToPeer() { + if (!this.peer) { return; } + const id = this.peerID; + console.log("trying to connect to", id) + const conn = this.peer.connect(id); + + conn.on('open', () => { + this.md.setMessagesLoading(false); + let i = 0; + console.log("peer connected") + + conn.on('data', (data) => { + if (typeof data === 'string') { return this.handleCommand(data); } + if (!Array.isArray(data)) { return; } + let time = 0; + let ts0 = 0; + (data as Array).forEach(msg => { + msg.tp = ID_TP_MAP[msg._id]; // _id goes from tracker + if (msg.tp === "timestamp") { + ts0 = ts0 || msg.timestamp + time = msg.timestamp - ts0; + return; + } + const tMsg: TimedMessage = Object.assign(msg, { + time, + _index: i, + }); + this.md.distributeMessage(tMsg, i++); + }); + }); + }); + conn.on('close', () => { + this.md.setMessagesLoading(true); + console.log('closed peer conn. Reconnecting...') + setTimeout(() => this.connectToPeer(), 300); // reconnect + }); + } + + + private get dataConnection(): DataConnection | null { + return this.peer?.connections[this.peerID]?.[0] || null; + } + + private get callConnection(): MediaConnection | null { + return this.peer?.connections[this.peerID]?.[1] || null; + } + + + private onCallEnd: null | (()=>void) = null; + private endCall = () => { + const conn = this.callConnection; + if (!conn || !conn.open) { return; } + conn.close(); //calls onCallEnd twice + this.dataConnection?.send("call_end"); // + this.onCallEnd?.(); + } + + private handleCommand(command: string) { + switch (command) { + case "call_end": + console.log("Call end recieved") + this.endCall(); + } + } + + private onMouseMoveShare = (e: MouseEvent ): void => { + const conn = this.dataConnection; + if (!conn || !conn.open) { return; } + // @ts-ignore ??? + const data = this.md._getInternalCoordinates(e); + conn.send({ x: Math.round(data.x), y: Math.round(data.y) }); // debounce? + } + + private calling: boolean = false; + call(localStream: MediaStream, onStream: (s: MediaStream)=>void, onClose: () => void, onError?: ()=> void): null | Function { + if (!this.peer || this.calling) { return null; } + const call = this.peer.call(this.peerID, localStream); + + console.log('calling...') + + this.calling = true; + call.on('stream', stream => { + onStream(stream); + // @ts-ignore ?? + this.md.overlay.addEventListener("mousemove", this.onMouseMoveShare) + }); + + this.onCallEnd = () => { + // @ts-ignore ?? + this.md.overlay.removeEventListener("mousemove", this.onMouseMoveShare); + this.calling = false; + onClose(); + } + call.on("close", this.onCallEnd); + call.on("error", (e) => { + console.error("PeerJS error (on call):", e) + this.onCallEnd?.(); + onError?.(); + }); + + return this.endCall; + } + + clear() { + this.peer?.destroy(); + } + +} diff --git a/frontend/app/player/singletone.js b/frontend/app/player/singletone.js index 3764e0480..de8be892f 100644 --- a/frontend/app/player/singletone.js +++ b/frontend/app/player/singletone.js @@ -67,7 +67,7 @@ export const attach = initCheck((...args) => instance.attach(...args)); export const markElement = initCheck((...args) => instance.marker && instance.marker.mark(...args)); export const scale = initCheck(() => instance.scale()); export const toggleInspectorMode = initCheck((...args) => instance.toggleInspectorMode(...args)); -export const callPeer = initCheck((...args) => instance.callPeer(...args)) +export const callPeer = initCheck((...args) => instance.assistManager.call(...args)) export const Controls = { jump, diff --git a/tracker/tracker-assist/package-lock.json b/tracker/tracker-assist/package-lock.json index 0fa82c623..4ccf9ba06 100644 --- a/tracker/tracker-assist/package-lock.json +++ b/tracker/tracker-assist/package-lock.json @@ -506,6 +506,11 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true }, + "npm-dragndrop": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/npm-dragndrop/-/npm-dragndrop-1.2.0.tgz", + "integrity": "sha1-bgUkAP7Yay8eP0csU4EPkjcRu7U=" + }, "p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", diff --git a/tracker/tracker-assist/package.json b/tracker/tracker-assist/package.json index 434f92ffe..581eb7cd4 100644 --- a/tracker/tracker-assist/package.json +++ b/tracker/tracker-assist/package.json @@ -20,6 +20,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { + "npm-dragndrop": "^1.2.0", "peerjs": "^1.3.2" }, "peerDependencies": { diff --git a/tracker/tracker-assist/src/CallWindow.ts b/tracker/tracker-assist/src/CallWindow.ts index 39445d467..69ae0f125 100644 --- a/tracker/tracker-assist/src/CallWindow.ts +++ b/tracker/tracker-assist/src/CallWindow.ts @@ -1,61 +1,199 @@ +const defaultView = ` + +
+ + + +
+ + + +
+
+` + +const V_WIDTH = 160; +const V_HEIGHT = 120; + export default class CallWindow { - private wrapper: HTMLDivElement; - private inputV: HTMLVideoElement; - private outputV: HTMLVideoElement; + private iframe: HTMLIFrameElement; + private vRemote: HTMLVideoElement | null = null; + private vLocal: HTMLVideoElement | null = null; + private soundBtn: HTMLButtonElement | null = null; + private videoBtn: HTMLButtonElement | null = null; constructor(endCall: () => void) { - this.wrapper = document.createElement('div'); - this.wrapper.style.position = "absolute" - this.wrapper.style.zIndex = "999999"; - this.outputV = document.createElement('video'); - this.outputV.height = 120 - this.outputV.width = 160 - this.outputV.autoplay = true - this.outputV.muted = true - this.inputV = document.createElement('video'); - this.inputV.height = 120 - this.inputV.width = 160 - this.inputV.autoplay = true - this.wrapper.appendChild(this.outputV); - this.wrapper.appendChild(this.inputV); + this.iframe = document.createElement('iframe'); + Object.assign(this.iframe.style, { + position: "absolute", + zIndex: "999999", + width: `${2*V_WIDTH}px`, + height: `${V_HEIGHT}px`, + borderRadius: ".25em .25em .4em .4em", + border: "4px rgba(0, 0, 0, .7)", + top: `calc(100% - ${V_HEIGHT + 20}px)`, + left: `calc(100% - ${2*V_WIDTH + 20}px)`, + }); + document.body.appendChild(this.iframe); - const endCallBtn = document.createElement('button'); + const doc = this.iframe.contentDocument + if (!doc) { + console.error("OpenReplay: CallWindow iframe document is not reachable.") + return; + } + + doc.body.innerHTML = defaultView; + + this.vLocal = doc.getElementById("vLocal") as HTMLVideoElement; + this.vLocal.height = V_HEIGHT + this.vLocal.width = V_WIDTH + this.vRemote = doc.getElementById("vRemote") as HTMLVideoElement; + this.vRemote.height = V_HEIGHT + this.vRemote.width = V_WIDTH + + const endCallBtn = doc.getElementById("endCallBtn") as HTMLButtonElement; endCallBtn.onclick = endCall; - this.wrapper.appendChild(endCallBtn); - - const soundBtn = document.createElement('button'); - soundBtn.onclick = () => this.toggleAudio(); - this.wrapper.appendChild(soundBtn); + + this.soundBtn = doc.getElementById("soundBtn") as HTMLButtonElement; + this.soundBtn.onclick = () => this.toggleAudio(); + + this.videoBtn = doc.getElementById("videoBtn") as HTMLButtonElement; + this.videoBtn.onclick = () => this.toggleVideo(); + + + + // TODO: better D'n'D + doc.body.setAttribute("draggable", "true"); + doc.body.ondragstart = (e) => { + if (!e.dataTransfer || !e.target) { return; } + e.dataTransfer.setDragImage(doc.body, e.clientX, e.clientY); + }; + doc.body.ondragend = e => { + Object.assign(this.iframe.style, { + left: `${e.clientX}px`, + top: `${e.clientY}px`, + }) + } - const videoButton = document.createElement('button'); - videoButton.onclick = () => this.toggleVideo(); - this.wrapper.appendChild(videoButton); - - document.body.appendChild(this.wrapper); - } private outputStream: MediaStream | null = null; setInputStream(iStream: MediaStream) { - this.inputV.srcObject = iStream; + if (!this.vRemote) { return; } + this.vRemote.srcObject = iStream; } setOutputStream(oStream: MediaStream) { + if (!this.vLocal) { return; } this.outputStream = oStream; - this.outputV.srcObject = oStream; + this.vLocal.srcObject = oStream; } - toggleAudio(flag?: boolean) { + toggleAudio() { + let enabled = true; this.outputStream?.getAudioTracks().forEach(track => { - track.enabled = typeof flag === 'boolean' ? flag : !track.enabled;; + enabled = enabled && !track.enabled; + track.enabled = enabled; }); + if (enabled) { + this.soundBtn?.classList.remove("muted"); + } else { + this.soundBtn?.classList.add("muted"); + } } - toggleVideo(flag?: boolean) { + toggleVideo() { + let enabled = true; this.outputStream?.getVideoTracks().forEach(track => { - track.enabled = typeof flag === 'boolean' ? flag : !track.enabled;; + enabled = enabled && !track.enabled; + track.enabled = enabled; }); + if (enabled) { + this.videoBtn?.classList.remove("off"); + } else { + this.videoBtn?.classList.add("off"); + } } remove() { - document.body.removeChild(this.wrapper); + if (this.iframe.parentElement) { + document.body.removeChild(this.iframe); + } } } \ No newline at end of file diff --git a/tracker/tracker-assist/src/Mouse.ts b/tracker/tracker-assist/src/Mouse.ts index 566fd2ef9..03558ce1c 100644 --- a/tracker/tracker-assist/src/Mouse.ts +++ b/tracker/tracker-assist/src/Mouse.ts @@ -25,6 +25,8 @@ export default class Mouse { } remove() { - document.body.removeChild(this.mouse); + if (this.mouse.parentElement) { + document.body.removeChild(this.mouse); + } } } \ No newline at end of file diff --git a/tracker/tracker-assist/src/confirm.ts b/tracker/tracker-assist/src/confirm.ts new file mode 100644 index 000000000..54d628182 --- /dev/null +++ b/tracker/tracker-assist/src/confirm.ts @@ -0,0 +1,76 @@ + +const declineIcon = ``; + +export default function confirm(text: string, styles?: Object): Promise { + return new Promise(resolve => { + + const wrapper = document.createElement('div'); + const popup = document.createElement('div'); + const p = document.createElement('p'); + p.innerText = text; + const buttons = document.createElement('div'); + const answerBtn = document.createElement('button'); + answerBtn.innerHTML = declineIcon.replace('fill="#ef5261"', 'fill="green"'); + const declineBtn = document.createElement('button'); + declineBtn.innerHTML = declineIcon; + buttons.appendChild(answerBtn); + buttons.appendChild(declineBtn); + popup.appendChild(p); + popup.appendChild(buttons); + + const btnStyles = { + borderRadius: "50%", + width: "20px", + height: "20px", + background: "transparent", + padding: 0, + margin: 0, + border: 0, + cursor: "pointer", + } + Object.assign(answerBtn.style, btnStyles); + Object.assign(declineBtn.style, btnStyles); + Object.assign(buttons.style, { + display: "flex", + alignItems: "center", + justifyContent: "space-evenly", + }); + + Object.assign(popup.style, { + position: "relative", + pointerEvents: "auto", + margin: "4em auto", + width: "90%", + maxWidth: "400px", + padding: "25px 30px", + background: "black", + opacity: ".75", + color: "white", + textAlign: "center", + borderRadius: ".25em .25em .4em .4em", + boxShadow: "0 0 20px rgb(0 0 0 / 20%)", + }, styles); + + Object.assign(wrapper.style, { + position: "fixed", + left: 0, + top: 0, + height: "100%", + width: "100%", + pointerEvents: "none", + }) + + + wrapper.appendChild(popup); + document.body.appendChild(wrapper); + + answerBtn.onclick = () => { + document.body.removeChild(wrapper); + resolve(true); + } + declineBtn.onclick = () => { + document.body.removeChild(wrapper); + resolve(false); + } + }) +} \ No newline at end of file diff --git a/tracker/tracker-assist/src/index.ts b/tracker/tracker-assist/src/index.ts index ea41d41ce..e324d5362 100644 --- a/tracker/tracker-assist/src/index.ts +++ b/tracker/tracker-assist/src/index.ts @@ -1,19 +1,25 @@ -import Peer from 'peerjs'; +import Peer, { MediaConnection } from 'peerjs'; import type { DataConnection } from 'peerjs'; import { App, Messages } from '@openreplay/tracker'; import type Message from '@openreplay/tracker'; import Mouse from './Mouse'; import CallWindow from './CallWindow'; +import confirm from './confirm'; + export interface Options { - + confirmText: string, + confirmStyle: Object, // Styles object } export default function(opts: Partial = {}) { const options: Options = Object.assign( - { }, + { + confirmText: "You have a call. Do you want to answer?", + confirmStyle: {}, + }, opts, ); return function(app: App | null) { @@ -22,6 +28,8 @@ export default function(opts: Partial = {}) { return; } + + let callingPeerDataConn app.attachStartCallback(function() { // @ts-ignore const peerID = `${app.getProjectKey()}-${app.getSessionID()}` @@ -31,8 +39,6 @@ export default function(opts: Partial = {}) { path: '/assist', port: 80,//443, }); - // peer.on('open', function(id) { - // }); console.log(peerID) peer.on('connection', function(conn) { console.log('connection') @@ -45,49 +51,64 @@ export default function(opts: Partial = {}) { conn.send(messages); }); app.start(); - //conn.send({}); - // conn.on('data', function(data) { - // console.log('Received', data); - // }); }); }); + let calling = false; peer.on('call', function(call) { - // ask client here. - - const answer = confirm("You have a call. Answer?") - if (!answer) return; - - const mouse = new Mouse(); - let callUI; - - navigator.mediaDevices.getUserMedia({video:true, audio:true}) - .then(oStream => { - const onClose = () => { - console.log("close call...") - call.close(); //? - mouse?.remove(); - callUI?.remove(); - oStream.getTracks().forEach(t => t.stop()); + const dataConn: DataConnection = peer + .connections[call.peer].find(c => c.type === 'data'); + if (calling) { + call.close(); + dataConn.send("call_end"); + return; + } + confirm(options.confirmText, options.confirmStyle).then(conf => { + if (!conf || !dataConn.open) { + call.close(); + dataConn.open && dataConn.send("call_end"); + return; } - call.on('close', onClose);// Doesnt' work on firefox - - call.answer(oStream); - callUI = new CallWindow(onClose); - callUI.setOutputStream(oStream); - call.on('stream', function(iStream) { - callUI.setInputStream(iStream); - Object.values(peer.connections).forEach((c: Array) => - c[0].on('data', data => { - mouse.move(data); - }) - ) + calling = true; + const mouse = new Mouse(); + let callUI; + + navigator.mediaDevices.getUserMedia({video:true, audio:true}) + .then(oStream => { + const onClose = () => { + console.log("close call...") + if (call.open) { call.close(); } + mouse?.remove(); + callUI?.remove(); + oStream.getTracks().forEach(t => t.stop()); + + calling = false; + if (dataConn.open) { + dataConn.send("call_end"); + } + } + dataConn?.on("close", onClose); + + call.answer(oStream); + call.on('close', onClose); // Works from time to time (peerjs bug) + call.on('error', onClose); // notify about error? + + callUI = new CallWindow(onClose); + callUI.setOutputStream(oStream); + call.on('stream', function(iStream) { + callUI.setInputStream(iStream); + dataConn?.on('data', (data: any) => { + if (data === "call_end") { + onClose(); + return; + } + if (call.open && data && typeof data.x === 'number' && typeof data.y === 'number') { + mouse.move(data); + } + }); + }); }); - - - }); - }); }); }