wip (tracker-assist & frontend): disconnections correct handling & plugin UI
This commit is contained in:
parent
2dcf2fd7b5
commit
3b85eb2f3e
11 changed files with 489 additions and 173 deletions
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
154
frontend/app/player/MessageDistributor/managers/AssistManager.ts
Normal file
154
frontend/app/player/MessageDistributor/managers/AssistManager.ts
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
5
tracker/tracker-assist/package-lock.json
generated
5
tracker/tracker-assist/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"npm-dragndrop": "^1.2.0",
|
||||
"peerjs": "^1.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -25,6 +25,8 @@ export default class Mouse {
|
|||
}
|
||||
|
||||
remove() {
|
||||
document.body.removeChild(this.mouse);
|
||||
if (this.mouse.parentElement) {
|
||||
document.body.removeChild(this.mouse);
|
||||
}
|
||||
}
|
||||
}
|
||||
76
tracker/tracker-assist/src/confirm.ts
Normal file
76
tracker/tracker-assist/src/confirm.ts
Normal 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);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue