wip (tracker-assist & frontend): disconnections correct handling & plugin UI

This commit is contained in:
ShiKhu 2021-06-30 22:52:42 +02:00
parent 2dcf2fd7b5
commit 3b85eb2f3e
11 changed files with 489 additions and 173 deletions

View file

@ -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<SetViewportSize & Timed> = new ListWalker([]);
private readonly pagesManager: PagesManager;
private readonly mouseManager: MouseManager;
private readonly assistManager: AssistManager;
private readonly scrollManager: ListWalker<SetViewportScroll & Timed> = 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<Message & { _id: number}>).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();
}
}

View file

@ -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;

View file

@ -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();

View file

@ -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<Message & { _id: number}>).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();
}
}

View file

@ -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,

View file

@ -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",

View file

@ -20,6 +20,7 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
"npm-dragndrop": "^1.2.0",
"peerjs": "^1.3.2"
},
"peerDependencies": {

View file

@ -1,61 +1,199 @@
const defaultView = `
<style>
* {
padding: 0;
margin: 0;
border: 0;
background: transparent;
}
#wrapper {
display: flex;
background-color: #333;
}
#controls {
display: flex;
justify-content: space-around;
position: fixed;
bottom: 0;
left: 0;
padding-bottom: 8px;
width: 100%;
}
button {
cursor: pointer;
border-radius: 50%;
width: 25px;
height: 25px;
position: relative;
opacity: .5;
transition: opacity .3s;
}
button.white {
background: white;
}
button:hover {
opacity: 1;
}
#soundBtn .bi-mic-mute {
display:none;
}
#soundBtn.muted .bi-mic-mute {
display: inline-block;
}
#soundBtn.muted .bi-mic {
display:none;
}
#videoBtn .bi-camera-video-off {
display:none;
}
#videoBtn.off .bi-camera-video-off {
display: inline-block;
}
#videoBtn.off .bi-camera-video {
display:none;
}
</style>
<div id="wrapper">
<video id="vLocal" autoplay muted ></video>
<video id="vRemote" autoplay ></video>
<div id="controls">
<button id="soundBtn" class="white">
<svg height="18" width="18" xmlns="http://www.w3.org/2000/svg" class="bi bi-mic" viewBox="0 0 16 16">
<path d="M3.5 6.5A.5.5 0 0 1 4 7v1a4 4 0 0 0 8 0V7a.5.5 0 0 1 1 0v1a5 5 0 0 1-4.5 4.975V15h3a.5.5 0 0 1 0 1h-7a.5.5 0 0 1 0-1h3v-2.025A5 5 0 0 1 3 8V7a.5.5 0 0 1 .5-.5z"/>
<path d="M10 8a2 2 0 1 1-4 0V3a2 2 0 1 1 4 0v5zM8 0a3 3 0 0 0-3 3v5a3 3 0 0 0 6 0V3a3 3 0 0 0-3-3z"/>
</svg>
<svg height="18" width="18" xmlns="http://www.w3.org/2000/svg" class="bi bi-mic-mute" viewBox="0 0 16 16">
<path d="M13 8c0 .564-.094 1.107-.266 1.613l-.814-.814A4.02 4.02 0 0 0 12 8V7a.5.5 0 0 1 1 0v1zm-5 4c.818 0 1.578-.245 2.212-.667l.718.719a4.973 4.973 0 0 1-2.43.923V15h3a.5.5 0 0 1 0 1h-7a.5.5 0 0 1 0-1h3v-2.025A5 5 0 0 1 3 8V7a.5.5 0 0 1 1 0v1a4 4 0 0 0 4 4zm3-9v4.879l-1-1V3a2 2 0 0 0-3.997-.118l-.845-.845A3.001 3.001 0 0 1 11 3z"/>
<path d="m9.486 10.607-.748-.748A2 2 0 0 1 6 8v-.878l-1-1V8a3 3 0 0 0 4.486 2.607zm-7.84-9.253 12 12 .708-.708-12-12-.708.708z"/>
</svg>
</button>
<button id="videoBtn" class="white">
<svg height="18" width="18" xmlns="http://www.w3.org/2000/svg" class="bi bi-camera-video" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M0 5a2 2 0 0 1 2-2h7.5a2 2 0 0 1 1.983 1.738l3.11-1.382A1 1 0 0 1 16 4.269v7.462a1 1 0 0 1-1.406.913l-3.111-1.382A2 2 0 0 1 9.5 13H2a2 2 0 0 1-2-2V5zm11.5 5.175 3.5 1.556V4.269l-3.5 1.556v4.35zM2 4a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h7.5a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H2z"/>
</svg>
<svg height="18" width="18" xmlns="http://www.w3.org/2000/svg" class="bi bi-camera-video-off" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M10.961 12.365a1.99 1.99 0 0 0 .522-1.103l3.11 1.382A1 1 0 0 0 16 11.731V4.269a1 1 0 0 0-1.406-.913l-3.111 1.382A2 2 0 0 0 9.5 3H4.272l.714 1H9.5a1 1 0 0 1 1 1v6a1 1 0 0 1-.144.518l.605.847zM1.428 4.18A.999.999 0 0 0 1 5v6a1 1 0 0 0 1 1h5.014l.714 1H2a2 2 0 0 1-2-2V5c0-.675.334-1.272.847-1.634l.58.814zM15 11.73l-3.5-1.555v-4.35L15 4.269v7.462zm-4.407 3.56-10-14 .814-.58 10 14-.814.58z"/>
</svg>
</button>
<button id="endCallBtn">
<svg height="25" width="25" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" ><g id="Circle_Grid" data-name="Circle Grid"><circle cx="64" cy="64" fill="#ef5261" r="64"/></g><g id="icon"><path d="m57.831 70.1c8.79 8.79 17.405 12.356 20.508 9.253l4.261-4.26a7.516 7.516 0 0 1 10.629 0l9.566 9.566a7.516 7.516 0 0 1 0 10.629l-7.453 7.453c-7.042 7.042-27.87-2.358-47.832-22.319-9.976-9.981-16.519-19.382-20.748-28.222s-5.086-16.091-1.567-19.61l7.453-7.453a7.516 7.516 0 0 1 10.629 0l9.566 9.563a7.516 7.516 0 0 1 0 10.629l-4.264 4.271c-3.103 3.1.462 11.714 9.252 20.5z" fill="#eeefee"/></g></svg>
</button>
</div>
</div>
`
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);
}
}
}

View file

@ -25,6 +25,8 @@ export default class Mouse {
}
remove() {
document.body.removeChild(this.mouse);
if (this.mouse.parentElement) {
document.body.removeChild(this.mouse);
}
}
}

View file

@ -0,0 +1,76 @@
const declineIcon = `<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 128 128" ><g id="Circle_Grid" data-name="Circle Grid"><circle cx="64" cy="64" fill="#ef5261" r="64"/></g><g id="icon"><path d="m57.831 70.1c8.79 8.79 17.405 12.356 20.508 9.253l4.261-4.26a7.516 7.516 0 0 1 10.629 0l9.566 9.566a7.516 7.516 0 0 1 0 10.629l-7.453 7.453c-7.042 7.042-27.87-2.358-47.832-22.319-9.976-9.981-16.519-19.382-20.748-28.222s-5.086-16.091-1.567-19.61l7.453-7.453a7.516 7.516 0 0 1 10.629 0l9.566 9.563a7.516 7.516 0 0 1 0 10.629l-4.264 4.271c-3.103 3.1.462 11.714 9.252 20.5z" fill="#eeefee"/></g></svg>`;
export default function confirm(text: string, styles?: Object): Promise<boolean> {
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);
}
})
}

View file

@ -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<Options> = {}) {
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<Options> = {}) {
return;
}
let callingPeerDataConn
app.attachStartCallback(function() {
// @ts-ignore
const peerID = `${app.getProjectKey()}-${app.getSessionID()}`
@ -31,8 +39,6 @@ export default function(opts: Partial<Options> = {}) {
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<Options> = {}) {
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<DataConnection>) =>
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);
}
});
});
});
});
});
});
}