feat(assist): 3.5.0: transition to WS, remote control separation + better customize configuration
This commit is contained in:
parent
ff0784bdd9
commit
967b885c16
7 changed files with 119 additions and 93 deletions
2
tracker/tracker-assist/.gitignore
vendored
2
tracker/tracker-assist/.gitignore
vendored
|
|
@ -1,6 +1,8 @@
|
|||
node_modules
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
lib
|
||||
cjs
|
||||
.cache
|
||||
*.cache
|
||||
*.DS_Store
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
src
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
tsconfig-cjs.json
|
||||
tsconfig.json
|
||||
.prettierrc.json
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import type { Socket } from 'socket.io-client';
|
||||
import io from 'socket.io-client';
|
||||
import Peer from 'peerjs';
|
||||
import type { Properties } from 'csstype';
|
||||
import { App } from '@openreplay/tracker';
|
||||
|
||||
import RequestLocalStream from './LocalStream.js';
|
||||
import Mouse from './Mouse.js';
|
||||
import CallWindow from './CallWindow.js';
|
||||
import ConfirmWindow from './ConfirmWindow.js';
|
||||
import RequestLocalStream from './LocalStream.js';
|
||||
import ConfirmWindow, { callConfirmDefault, controlConfirmDefault } from './ConfirmWindow.js';
|
||||
import type { Options as ConfirmOptions } from './ConfirmWindow.js';
|
||||
|
||||
|
||||
//@ts-ignore peerjs hack for webpack5 (?!) TODO: ES/node modules;
|
||||
|
|
@ -15,9 +17,13 @@ Peer = Peer.default || Peer;
|
|||
export interface Options {
|
||||
onAgentConnect: () => ((()=>{}) | void),
|
||||
onCallStart: () => ((()=>{}) | void),
|
||||
confirmText: string,
|
||||
confirmStyle: Object, // Styles object
|
||||
session_calling_peer_key: string,
|
||||
callConfirm: ConfirmOptions,
|
||||
controlConfirm: ConfirmOptions,
|
||||
|
||||
confirmText?: string, // @depricated
|
||||
confirmStyle?: Properties, // @depricated
|
||||
|
||||
config: RTCConfiguration,
|
||||
}
|
||||
|
||||
|
|
@ -44,10 +50,22 @@ export default class Assist {
|
|||
private callingState: CallingState = CallingState.False
|
||||
|
||||
private agents: Record<string, Agent> = {}
|
||||
private readonly options: Options
|
||||
constructor(
|
||||
private readonly app: App,
|
||||
private readonly options: Options,
|
||||
private readonly noSecureMode: boolean = false) {
|
||||
options?: Partial<Options>,
|
||||
private readonly noSecureMode: boolean = false,
|
||||
) {
|
||||
this.options = Object.assign({
|
||||
session_calling_peer_key: "__openreplay_calling_peer",
|
||||
config: null,
|
||||
onCallStart: ()=>{},
|
||||
onAgentConnect: ()=>{},
|
||||
callConfirm: {},
|
||||
controlConfirm: {}, // TODO: clear options passing/merging/overriting
|
||||
},
|
||||
options,
|
||||
);
|
||||
app.attachStartCallback(() => {
|
||||
if (this.assistDemandedRestart) { return; }
|
||||
this.onStart()
|
||||
|
|
@ -123,7 +141,7 @@ export default class Assist {
|
|||
return
|
||||
}
|
||||
controllingAgent = id
|
||||
confirmRC = new ConfirmWindow("Allow remote control?")
|
||||
confirmRC = new ConfirmWindow(controlConfirmDefault(this.options.controlConfirm))
|
||||
confirmRC.mount().then(allowed => {
|
||||
if (allowed) { // TODO: per agent id
|
||||
mouse.mount()
|
||||
|
|
@ -206,7 +224,10 @@ export default class Assist {
|
|||
confirmAnswer = Promise.resolve(true)
|
||||
} else {
|
||||
setCallingState(CallingState.Requesting)
|
||||
confirmCall = new ConfirmWindow(this.options.confirmText, this.options.confirmStyle)
|
||||
confirmCall = new ConfirmWindow(callConfirmDefault(this.options.callConfirm || {
|
||||
text: this.options.confirmText,
|
||||
style: this.options.confirmStyle,
|
||||
}))
|
||||
confirmAnswer = confirmCall.mount()
|
||||
this.onRemoteCallEnd = () => { // if call cancelled by a caller before confirmation
|
||||
app.debug.log("Received call_end during confirm window opened")
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
import type { DataConnection } from 'peerjs';
|
||||
|
||||
// TODO: proper Message type export from tracker in 3.5.0
|
||||
interface Message {
|
||||
encode(w: any): boolean;
|
||||
}
|
||||
|
||||
// 16kb should be max according to specification
|
||||
// 64kb chrome
|
||||
const crOrFf: boolean =
|
||||
typeof navigator !== "undefined" &&
|
||||
(navigator.userAgent.indexOf("Chrom") !== -1 || // Chrome && Chromium
|
||||
navigator.userAgent.indexOf("Firefox") !== -1);
|
||||
|
||||
const MESSAGES_PER_SEND = crOrFf ? 200 : 50
|
||||
|
||||
// Bffering required in case of webRTC
|
||||
export default class BufferingConnection {
|
||||
private readonly buffer: Message[][] = []
|
||||
private buffering: boolean = false
|
||||
|
||||
constructor(readonly conn: DataConnection,
|
||||
private readonly msgsPerSend: number = MESSAGES_PER_SEND){}
|
||||
private sendNext() {
|
||||
if (this.buffer.length) {
|
||||
setTimeout(() => {
|
||||
this.conn.send(this.buffer.shift())
|
||||
this.sendNext()
|
||||
}, 15)
|
||||
} else {
|
||||
this.buffering = false
|
||||
}
|
||||
}
|
||||
|
||||
send(messages: Message[]) {
|
||||
if (!this.conn.open) { return; }
|
||||
let i = 0;
|
||||
//@ts-ignore
|
||||
messages=messages.filter(m => m._id !== 39)
|
||||
while (i < messages.length) {
|
||||
|
||||
this.buffer.push(messages.slice(i, i+=this.msgsPerSend))
|
||||
}
|
||||
if (!this.buffering) {
|
||||
this.buffering = true
|
||||
this.sendNext();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +1,83 @@
|
|||
import type { Properties } from 'csstype';
|
||||
|
||||
const declineIcon = `<svg xmlns="http://www.w3.org/2000/svg" height="22" width="22" 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>`;
|
||||
import { declineCall, acceptCall, cross, remoteControl } from './icons.js'
|
||||
|
||||
export default class ConfirmWindow {
|
||||
private wrapper: HTMLDivElement;
|
||||
type ButtonOptions = HTMLButtonElement | string | {
|
||||
innerHTML: string,
|
||||
style?: Properties,
|
||||
}
|
||||
|
||||
constructor(text: string, styles?: Object) {
|
||||
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: "22px",
|
||||
height: "22px",
|
||||
// TODO: common strategy for InputOptions/defaultOptions merging
|
||||
interface ConfirmWindowOptions {
|
||||
text: string,
|
||||
style?: Properties,
|
||||
confirmBtn: ButtonOptions,
|
||||
declineBtn: ButtonOptions,
|
||||
}
|
||||
|
||||
export type Options = string | Partial<ConfirmWindowOptions>
|
||||
|
||||
function confirmDefault(
|
||||
opts: Options,
|
||||
confirmBtn: ButtonOptions,
|
||||
declineBtn: ButtonOptions,
|
||||
text: string,
|
||||
): ConfirmWindowOptions {
|
||||
const isStr = typeof opts === "string"
|
||||
return Object.assign({
|
||||
text: isStr ? opts : text,
|
||||
confirmBtn,
|
||||
declineBtn,
|
||||
}, isStr ? undefined : opts)
|
||||
}
|
||||
|
||||
export const callConfirmDefault = (opts: Options) =>
|
||||
confirmDefault(opts, acceptCall, declineCall, "You have an incoming call. Do you want to answer?")
|
||||
export const controlConfirmDefault = (opts: Options) =>
|
||||
confirmDefault(opts, remoteControl, cross, "Allow remote control?")
|
||||
|
||||
function makeButton(options: ButtonOptions): HTMLButtonElement {
|
||||
if (options instanceof HTMLButtonElement) {
|
||||
return options
|
||||
}
|
||||
const btn = document.createElement('button')
|
||||
Object.assign(btn.style, {
|
||||
background: "transparent",
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
border: 0,
|
||||
cursor: "pointer",
|
||||
borderRadius: "50%",
|
||||
width: "22px",
|
||||
height: "22px",
|
||||
color: "white", // TODO: nice text button in case when only text is passed
|
||||
})
|
||||
if (typeof options === "string") {
|
||||
btn.innerHTML = options
|
||||
} else {
|
||||
btn.innerHTML = options.innerHTML
|
||||
Object.assign(btn.style, options.style)
|
||||
}
|
||||
Object.assign(answerBtn.style, btnStyles);
|
||||
Object.assign(declineBtn.style, btnStyles);
|
||||
return btn
|
||||
}
|
||||
|
||||
export default class ConfirmWindow {
|
||||
private wrapper: HTMLDivElement;
|
||||
|
||||
constructor(options: ConfirmWindowOptions) {
|
||||
const wrapper = document.createElement('div');
|
||||
const popup = document.createElement('div');
|
||||
const p = document.createElement('p');
|
||||
p.innerText = options.text;
|
||||
const buttons = document.createElement('div');
|
||||
const confirmBtn = makeButton(options.confirmBtn);
|
||||
const declineBtn = makeButton(options.declineBtn);
|
||||
buttons.appendChild(confirmBtn);
|
||||
buttons.appendChild(declineBtn);
|
||||
popup.appendChild(p);
|
||||
popup.appendChild(buttons);
|
||||
|
||||
Object.assign(buttons.style, {
|
||||
marginTop: "10px",
|
||||
display: "flex",
|
||||
|
|
@ -51,7 +98,7 @@ export default class ConfirmWindow {
|
|||
textAlign: "center",
|
||||
borderRadius: ".25em .25em .4em .4em",
|
||||
boxShadow: "0 0 20px rgb(0 0 0 / 20%)",
|
||||
}, styles);
|
||||
}, options.style);
|
||||
|
||||
Object.assign(wrapper.style, {
|
||||
position: "fixed",
|
||||
|
|
@ -66,7 +113,7 @@ export default class ConfirmWindow {
|
|||
wrapper.appendChild(popup);
|
||||
this.wrapper = wrapper;
|
||||
|
||||
answerBtn.onclick = () => {
|
||||
confirmBtn.onclick = () => {
|
||||
this._remove();
|
||||
this.resolve(true);
|
||||
}
|
||||
|
|
|
|||
14
tracker/tracker-assist/src/icons.ts
Normal file
14
tracker/tracker-assist/src/icons.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
|
||||
// TODO: something with these big strings in bundle?
|
||||
|
||||
export const declineCall = `<svg xmlns="http://www.w3.org/2000/svg" height="22" width="22" 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 const acceptCall = declineCall.replace('fill="#ef5261"', 'fill="green"')
|
||||
|
||||
export const cross = `<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-x-lg" viewBox="0 0 16 16" fill="#ef5261">
|
||||
<path fill-rule="evenodd" d="M13.854 2.146a.5.5 0 0 1 0 .708l-11 11a.5.5 0 0 1-.708-.708l11-11a.5.5 0 0 1 .708 0Z"/>
|
||||
<path fill-rule="evenodd" d="M2.146 2.146a.5.5 0 0 0 0 .708l11 11a.5.5 0 0 0 .708-.708l-11-11a.5.5 0 0 0-.708 0Z"/>
|
||||
</svg>`
|
||||
|
||||
export const remoteControl = `<svg fill="green" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M638.59 368.22l-33.37-211.59c-8.86-50.26-48.4-90.77-100.66-103.13h-.07a803.14 803.14 0 0 0-369 0C83.17 65.86 43.64 106.36 34.78 156.63L1.41 368.22C-8.9 426.73 38.8 480 101.51 480c49.67 0 93.77-30.07 109.48-74.64l7.52-21.36h203l7.49 21.36C444.72 449.93 488.82 480 538.49 480c62.71 0 110.41-53.27 100.1-111.78zm-45.11 54.88c-13.28 15.82-33.33 24.9-55 24.9-36.2 0-68.07-21.41-79.29-53.27l-7.53-21.36-7.52-21.37H195.86l-7.53 21.37-7.53 21.36C169.58 426.59 137.71 448 101.51 448c-21.66 0-41.71-9.08-55-24.9A59.93 59.93 0 0 1 33 373.2l33.28-211c6.66-37.7 36.72-68.14 76.53-77.57a771.07 771.07 0 0 1 354.38 0c39.84 9.42 69.87 39.86 76.42 77l33.47 212.15c3.11 17.64-1.72 35.16-13.6 49.32zm-339.3-218.74h-42.54v-42.54a9.86 9.86 0 0 0-9.82-9.82h-19.64a9.86 9.86 0 0 0-9.82 9.82v42.54h-42.54a9.86 9.86 0 0 0-9.82 9.82v19.64a9.86 9.86 0 0 0 9.82 9.82h42.54v42.54a9.86 9.86 0 0 0 9.82 9.82h19.64a9.86 9.86 0 0 0 9.82-9.82v-42.54h42.54a9.86 9.86 0 0 0 9.82-9.82v-19.64a9.86 9.86 0 0 0-9.82-9.82zM416 224a32 32 0 1 0 32 32 32 32 0 0 0-32-32zm64-64a32 32 0 1 0 32 32 32 32 0 0 0-32-32z"/></svg>`
|
||||
|
|
@ -6,17 +6,6 @@ import Assist from './Assist.js'
|
|||
|
||||
|
||||
export default function(opts?: Partial<Options>) {
|
||||
const options: Options = Object.assign(
|
||||
{
|
||||
confirmText: "You have an incoming call. Do you want to answer?",
|
||||
confirmStyle: {},
|
||||
session_calling_peer_key: "__openreplay_calling_peer",
|
||||
config: null,
|
||||
onCallStart: ()=>{},
|
||||
onAgentConnect: ()=>{},
|
||||
},
|
||||
opts,
|
||||
);
|
||||
return function(app: App | null, appOptions: { __DISABLE_SECURE_MODE?: boolean } = {}) {
|
||||
// @ts-ignore
|
||||
if (app === null || !navigator?.mediaDevices?.getUserMedia) { // 93.04% browsers
|
||||
|
|
@ -27,7 +16,7 @@ export default function(opts?: Partial<Options>) {
|
|||
return
|
||||
}
|
||||
app.notify.log("OpenReplay Assist initializing.")
|
||||
const assist = new Assist(app, options, appOptions.__DISABLE_SECURE_MODE)
|
||||
const assist = new Assist(app, opts, appOptions.__DISABLE_SECURE_MODE)
|
||||
app.debug.log(assist)
|
||||
return assist
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue