feat: canvas support [assist] (#1641)
* feat(tracker/ui): start canvas support * feat(tracker): slpeer -> peerjs for canvas streams * fix(ui): fix agent canvas peer id * fix(ui): fix agent canvas peer id * fix(ui): fix peer removal * feat(tracker): canvas recorder * feat(tracker): canvas recorder * feat(tracker): canvas recorder * feat(tracker): canvas recorder * feat(ui): canvas support for ui * fix(tracker): fix falling tests * feat(ui): replay canvas in video * feat(ui): refactor video streaming to draw on canvas * feat(ui): 10hz check for canvas replay * feat(ui): fix for tests * feat(ui): fix for tests * feat(ui): fix for tests * feat(ui): fix for tests cov * feat(ui): mroe test coverage * fix(ui): styling * fix(tracker): support backend settings for canvas
This commit is contained in:
parent
05a07ab525
commit
07046cc2fb
48 changed files with 1028 additions and 216 deletions
8
.github/workflows/tracker-tests.yaml
vendored
8
.github/workflows/tracker-tests.yaml
vendored
|
|
@ -47,10 +47,6 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
cd tracker/tracker
|
cd tracker/tracker
|
||||||
bun install
|
bun install
|
||||||
- name: (TA) Setup Testing packages
|
|
||||||
run: |
|
|
||||||
cd tracker/tracker-assist
|
|
||||||
bun install
|
|
||||||
- name: Jest tests
|
- name: Jest tests
|
||||||
run: |
|
run: |
|
||||||
cd tracker/tracker
|
cd tracker/tracker
|
||||||
|
|
@ -59,6 +55,10 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
cd tracker/tracker
|
cd tracker/tracker
|
||||||
bun run build
|
bun run build
|
||||||
|
- name: (TA) Setup Testing packages
|
||||||
|
run: |
|
||||||
|
cd tracker/tracker-assist
|
||||||
|
bun install
|
||||||
- name: (TA) Jest tests
|
- name: (TA) Jest tests
|
||||||
run: |
|
run: |
|
||||||
cd tracker/tracker-assist
|
cd tracker/tracker-assist
|
||||||
|
|
|
||||||
|
|
@ -10,5 +10,5 @@ func IsIOSType(id int) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsDOMType(id int) bool {
|
func IsDOMType(id int) bool {
|
||||||
return 0 == id || 4 == id || 5 == id || 6 == id || 7 == id || 8 == id || 9 == id || 10 == id || 11 == id || 12 == id || 13 == id || 14 == id || 15 == id || 16 == id || 18 == id || 19 == id || 20 == id || 37 == id || 38 == id || 49 == id || 50 == id || 51 == id || 54 == id || 55 == id || 57 == id || 58 == id || 59 == id || 60 == id || 61 == id || 67 == id || 69 == id || 70 == id || 71 == id || 72 == id || 73 == id || 74 == id || 75 == id || 76 == id || 77 == id || 113 == id || 114 == id || 117 == id || 118 == id || 93 == id || 96 == id || 100 == id || 101 == id || 102 == id || 103 == id || 104 == id || 105 == id || 106 == id || 111 == id
|
return 0 == id || 4 == id || 5 == id || 6 == id || 7 == id || 8 == id || 9 == id || 10 == id || 11 == id || 12 == id || 13 == id || 14 == id || 15 == id || 16 == id || 18 == id || 19 == id || 20 == id || 37 == id || 38 == id || 49 == id || 50 == id || 51 == id || 54 == id || 55 == id || 57 == id || 58 == id || 59 == id || 60 == id || 61 == id || 67 == id || 69 == id || 70 == id || 71 == id || 72 == id || 73 == id || 74 == id || 75 == id || 76 == id || 77 == id || 113 == id || 114 == id || 117 == id || 118 == id || 119 == id || 93 == id || 96 == id || 100 == id || 101 == id || 102 == id || 103 == id || 104 == id || 105 == id || 106 == id || 111 == id
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,7 @@ const (
|
||||||
MsgResourceTiming = 116
|
MsgResourceTiming = 116
|
||||||
MsgTabChange = 117
|
MsgTabChange = 117
|
||||||
MsgTabData = 118
|
MsgTabData = 118
|
||||||
|
MsgCanvasNode = 119
|
||||||
MsgIssueEvent = 125
|
MsgIssueEvent = 125
|
||||||
MsgSessionEnd = 126
|
MsgSessionEnd = 126
|
||||||
MsgSessionSearch = 127
|
MsgSessionSearch = 127
|
||||||
|
|
@ -2245,6 +2246,29 @@ func (msg *TabData) TypeID() int {
|
||||||
return 118
|
return 118
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CanvasNode struct {
|
||||||
|
message
|
||||||
|
NodeId string
|
||||||
|
Timestamp uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *CanvasNode) Encode() []byte {
|
||||||
|
buf := make([]byte, 21+len(msg.NodeId))
|
||||||
|
buf[0] = 119
|
||||||
|
p := 1
|
||||||
|
p = WriteString(msg.NodeId, buf, p)
|
||||||
|
p = WriteUint(msg.Timestamp, buf, p)
|
||||||
|
return buf[:p]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *CanvasNode) Decode() Message {
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *CanvasNode) TypeID() int {
|
||||||
|
return 119
|
||||||
|
}
|
||||||
|
|
||||||
type IssueEvent struct {
|
type IssueEvent struct {
|
||||||
message
|
message
|
||||||
MessageID uint64
|
MessageID uint64
|
||||||
|
|
|
||||||
|
|
@ -1365,6 +1365,18 @@ func DecodeTabData(reader BytesReader) (Message, error) {
|
||||||
return msg, err
|
return msg, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DecodeCanvasNode(reader BytesReader) (Message, error) {
|
||||||
|
var err error = nil
|
||||||
|
msg := &CanvasNode{}
|
||||||
|
if msg.NodeId, err = reader.ReadString(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if msg.Timestamp, err = reader.ReadUint(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return msg, err
|
||||||
|
}
|
||||||
|
|
||||||
func DecodeIssueEvent(reader BytesReader) (Message, error) {
|
func DecodeIssueEvent(reader BytesReader) (Message, error) {
|
||||||
var err error = nil
|
var err error = nil
|
||||||
msg := &IssueEvent{}
|
msg := &IssueEvent{}
|
||||||
|
|
@ -1993,6 +2005,8 @@ func ReadMessage(t uint64, reader BytesReader) (Message, error) {
|
||||||
return DecodeTabChange(reader)
|
return DecodeTabChange(reader)
|
||||||
case 118:
|
case 118:
|
||||||
return DecodeTabData(reader)
|
return DecodeTabData(reader)
|
||||||
|
case 119:
|
||||||
|
return DecodeCanvasNode(reader)
|
||||||
case 125:
|
case 125:
|
||||||
return DecodeIssueEvent(reader)
|
return DecodeIssueEvent(reader)
|
||||||
case 126:
|
case 126:
|
||||||
|
|
|
||||||
10
codecov.yml
Normal file
10
codecov.yml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
ignore:
|
||||||
|
- "**/*/*.gen.ts"
|
||||||
|
- "**/*/coverage.xml"
|
||||||
|
- "**/*/coverage-final.json"
|
||||||
|
- "**/*/coverage/**"
|
||||||
|
- "**/*/node_modules/**"
|
||||||
|
- "**/*/dist/**"
|
||||||
|
- "**/*/build/**"
|
||||||
|
- "**/*/.test.*"
|
||||||
|
- "**/*/version.ts"
|
||||||
|
|
@ -788,6 +788,14 @@ class TabData(Message):
|
||||||
self.tab_id = tab_id
|
self.tab_id = tab_id
|
||||||
|
|
||||||
|
|
||||||
|
class CanvasNode(Message):
|
||||||
|
__id__ = 119
|
||||||
|
|
||||||
|
def __init__(self, node_id, timestamp):
|
||||||
|
self.node_id = node_id
|
||||||
|
self.timestamp = timestamp
|
||||||
|
|
||||||
|
|
||||||
class IssueEvent(Message):
|
class IssueEvent(Message):
|
||||||
__id__ = 125
|
__id__ = 125
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1164,6 +1164,17 @@ cdef class TabData(PyMessage):
|
||||||
self.tab_id = tab_id
|
self.tab_id = tab_id
|
||||||
|
|
||||||
|
|
||||||
|
cdef class CanvasNode(PyMessage):
|
||||||
|
cdef public int __id__
|
||||||
|
cdef public str node_id
|
||||||
|
cdef public unsigned long timestamp
|
||||||
|
|
||||||
|
def __init__(self, str node_id, unsigned long timestamp):
|
||||||
|
self.__id__ = 119
|
||||||
|
self.node_id = node_id
|
||||||
|
self.timestamp = timestamp
|
||||||
|
|
||||||
|
|
||||||
cdef class IssueEvent(PyMessage):
|
cdef class IssueEvent(PyMessage):
|
||||||
cdef public int __id__
|
cdef public int __id__
|
||||||
cdef public unsigned long message_id
|
cdef public unsigned long message_id
|
||||||
|
|
|
||||||
|
|
@ -711,6 +711,12 @@ class MessageCodec(Codec):
|
||||||
tab_id=self.read_string(reader)
|
tab_id=self.read_string(reader)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if message_id == 119:
|
||||||
|
return CanvasNode(
|
||||||
|
node_id=self.read_string(reader),
|
||||||
|
timestamp=self.read_uint(reader)
|
||||||
|
)
|
||||||
|
|
||||||
if message_id == 125:
|
if message_id == 125:
|
||||||
return IssueEvent(
|
return IssueEvent(
|
||||||
message_id=self.read_uint(reader),
|
message_id=self.read_uint(reader),
|
||||||
|
|
|
||||||
|
|
@ -809,6 +809,12 @@ cdef class MessageCodec:
|
||||||
tab_id=self.read_string(reader)
|
tab_id=self.read_string(reader)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if message_id == 119:
|
||||||
|
return CanvasNode(
|
||||||
|
node_id=self.read_string(reader),
|
||||||
|
timestamp=self.read_uint(reader)
|
||||||
|
)
|
||||||
|
|
||||||
if message_id == 125:
|
if message_id == 125:
|
||||||
return IssueEvent(
|
return IssueEvent(
|
||||||
message_id=self.read_uint(reader),
|
message_id=self.read_uint(reader),
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,10 @@
|
||||||
],
|
],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"babel-plugin-react-require",
|
"babel-plugin-react-require",
|
||||||
[ "@babel/plugin-proposal-private-property-in-object", { "loose": true } ],
|
["@babel/plugin-transform-private-property-in-object", { "loose":true } ],
|
||||||
[ "@babel/plugin-transform-runtime", { "regenerator": true } ],
|
[ "@babel/plugin-transform-runtime", { "regenerator": true } ],
|
||||||
[ "@babel/plugin-proposal-decorators", { "legacy":true } ],
|
[ "@babel/plugin-proposal-decorators", { "legacy":true } ],
|
||||||
[ "@babel/plugin-proposal-class-properties", { "loose":true } ],
|
[ "@babel/plugin-transform-class-properties", { "loose":true } ],
|
||||||
[ "@babel/plugin-proposal-private-methods", { "loose": true }]
|
[ "@babel/plugin-transform-private-methods", { "loose": true }]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -236,6 +236,10 @@ export default class MessageManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getNode(id: number) {
|
||||||
|
return this.tabs[this.activeTab]?.getNode(id);
|
||||||
|
}
|
||||||
|
|
||||||
public changeTab(tabId: string) {
|
public changeTab(tabId: string) {
|
||||||
this.activeTab = tabId;
|
this.activeTab = tabId;
|
||||||
this.state.update({ currentTab: tabId });
|
this.state.update({ currentTab: tabId });
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,7 @@ export default class Screen {
|
||||||
return this.parentElement
|
return this.parentElement
|
||||||
}
|
}
|
||||||
|
|
||||||
setBorderStyle(style: { border: string }) {
|
setBorderStyle(style: { outline: string }) {
|
||||||
return Object.assign(this.screen.style, style)
|
return Object.assign(this.screen.style, style)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,28 @@
|
||||||
|
import type { Store } from "Player";
|
||||||
|
import { getResourceFromNetworkRequest, getResourceFromResourceTiming, Log, ResourceType } from "Player";
|
||||||
import ListWalker from "Player/common/ListWalker";
|
import ListWalker from "Player/common/ListWalker";
|
||||||
|
import Lists, { INITIAL_STATE as LISTS_INITIAL_STATE, InitialLists, State as ListsState } from "Player/web/Lists";
|
||||||
|
import CanvasManager from "Player/web/managers/CanvasManager";
|
||||||
|
import { VElement } from "Player/web/managers/DOM/VirtualDOM";
|
||||||
|
import PagesManager from "Player/web/managers/PagesManager";
|
||||||
|
import PerformanceTrackManager from "Player/web/managers/PerformanceTrackManager";
|
||||||
|
import WindowNodeCounter from "Player/web/managers/WindowNodeCounter";
|
||||||
import {
|
import {
|
||||||
|
CanvasNode,
|
||||||
ConnectionInformation,
|
ConnectionInformation,
|
||||||
Message, MType, ResourceTiming,
|
Message,
|
||||||
|
MType,
|
||||||
|
ResourceTiming,
|
||||||
SetPageLocation,
|
SetPageLocation,
|
||||||
SetViewportScroll,
|
SetViewportScroll,
|
||||||
SetViewportSize
|
SetViewportSize
|
||||||
} from "Player/web/messages";
|
} from "Player/web/messages";
|
||||||
import PerformanceTrackManager from "Player/web/managers/PerformanceTrackManager";
|
import { isDOMType } from "Player/web/messages/filters.gen";
|
||||||
import WindowNodeCounter from "Player/web/managers/WindowNodeCounter";
|
import Screen from "Player/web/Screen/Screen";
|
||||||
import PagesManager from "Player/web/managers/PagesManager";
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { Decoder } from "syncod";
|
import { Decoder } from "syncod";
|
||||||
import Lists, { InitialLists, INITIAL_STATE as LISTS_INITIAL_STATE, State as ListsState } from "Player/web/Lists";
|
|
||||||
import type { Store } from 'Player';
|
|
||||||
import Screen from "Player/web/Screen/Screen";
|
|
||||||
import { TYPES as EVENT_TYPES } from "Types/session/event";
|
import { TYPES as EVENT_TYPES } from "Types/session/event";
|
||||||
import type { PerformanceChartPoint } from './managers/PerformanceTrackManager';
|
import type { PerformanceChartPoint } from "./managers/PerformanceTrackManager";
|
||||||
import { getResourceFromNetworkRequest, getResourceFromResourceTiming, Log, ResourceType } from "Player";
|
|
||||||
import { isDOMType } from "Player/web/messages/filters.gen";
|
|
||||||
|
|
||||||
export interface TabState extends ListsState {
|
export interface TabState extends ListsState {
|
||||||
performanceAvailability?: PerformanceTrackManager['availability']
|
performanceAvailability?: PerformanceTrackManager['availability']
|
||||||
|
|
@ -57,6 +62,8 @@ export default class TabSessionManager {
|
||||||
public readonly decoder = new Decoder();
|
public readonly decoder = new Decoder();
|
||||||
private lists: Lists;
|
private lists: Lists;
|
||||||
private navigationStartOffset = 0
|
private navigationStartOffset = 0
|
||||||
|
private canvasManagers: { [key: string]: { manager: CanvasManager, start: number, running: boolean } } = {}
|
||||||
|
private canvasReplayWalker: ListWalker<CanvasNode> = new ListWalker();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly session: any,
|
private readonly session: any,
|
||||||
|
|
@ -76,6 +83,10 @@ export default class TabSessionManager {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getNode = (id: number) => {
|
||||||
|
return this.pagesManager.getNode(id)
|
||||||
|
}
|
||||||
|
|
||||||
public updateLists(lists: Partial<InitialLists>) {
|
public updateLists(lists: Partial<InitialLists>) {
|
||||||
Object.keys(lists).forEach((key: 'event' | 'stack' | 'exceptions') => {
|
Object.keys(lists).forEach((key: 'event' | 'stack' | 'exceptions') => {
|
||||||
const currentList = this.lists.lists[key]
|
const currentList = this.lists.lists[key]
|
||||||
|
|
@ -141,6 +152,23 @@ export default class TabSessionManager {
|
||||||
|
|
||||||
distributeMessage(msg: Message): void {
|
distributeMessage(msg: Message): void {
|
||||||
switch (msg.tp) {
|
switch (msg.tp) {
|
||||||
|
case MType.CanvasNode:
|
||||||
|
const managerId = `${msg.timestamp}_${msg.nodeId}`;
|
||||||
|
if (!this.canvasManagers[managerId]) {
|
||||||
|
const filename = `${managerId}.mp4`;
|
||||||
|
const delta = msg.timestamp - this.sessionStart;
|
||||||
|
const fileUrl = this.session.canvasURL.find((url: string) => url.includes(filename));
|
||||||
|
const manager = new CanvasManager(
|
||||||
|
msg.nodeId,
|
||||||
|
delta,
|
||||||
|
fileUrl,
|
||||||
|
this.getNode as (id: number) => VElement | undefined
|
||||||
|
);
|
||||||
|
this.canvasManagers[managerId] = { manager, start: msg.timestamp, running: false };
|
||||||
|
this.canvasReplayWalker.append(msg);
|
||||||
|
|
||||||
|
}
|
||||||
|
break;
|
||||||
case MType.SetPageLocation:
|
case MType.SetPageLocation:
|
||||||
this.locationManager.append(msg);
|
this.locationManager.append(msg);
|
||||||
if (msg.navigationStart > 0) {
|
if (msg.navigationStart > 0) {
|
||||||
|
|
@ -289,6 +317,16 @@ export default class TabSessionManager {
|
||||||
if (!!lastScroll && this.screen.window) {
|
if (!!lastScroll && this.screen.window) {
|
||||||
this.screen.window.scrollTo(lastScroll.x, lastScroll.y);
|
this.screen.window.scrollTo(lastScroll.x, lastScroll.y);
|
||||||
}
|
}
|
||||||
|
const canvasMsg = this.canvasReplayWalker.moveGetLast(t)
|
||||||
|
if (canvasMsg) {
|
||||||
|
this.canvasManagers[`${canvasMsg.timestamp}_${canvasMsg.nodeId}`].manager.startVideo();
|
||||||
|
this.canvasManagers[`${canvasMsg.timestamp}_${canvasMsg.nodeId}`].running = true;
|
||||||
|
}
|
||||||
|
const runningManagers = Object.keys(this.canvasManagers).filter((key) => this.canvasManagers[key].running);
|
||||||
|
runningManagers.forEach((key) => {
|
||||||
|
const manager = this.canvasManagers[key].manager;
|
||||||
|
manager.move(t);
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ export default class WebLivePlayer extends WebPlayer {
|
||||||
this.screen,
|
this.screen,
|
||||||
config,
|
config,
|
||||||
wpState,
|
wpState,
|
||||||
|
(id) => this.messageManager.getNode(id),
|
||||||
uiErrorHandler,
|
uiErrorHandler,
|
||||||
)
|
)
|
||||||
this.assistManager.connect(session.agentToken!, agentId, projectId)
|
this.assistManager.connect(session.agentToken!, agentId, projectId)
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,16 @@
|
||||||
|
import MessageManager from 'Player/web/MessageManager';
|
||||||
import type { Socket } from 'socket.io-client';
|
import type { Socket } from 'socket.io-client';
|
||||||
import type Screen from '../Screen/Screen';
|
import type Screen from '../Screen/Screen';
|
||||||
import type { Store } from '../../common/types'
|
import type { Store } from '../../common/types';
|
||||||
import type { Message } from '../messages';
|
import type { Message } from '../messages';
|
||||||
import MStreamReader from '../messages/MStreamReader';
|
import MStreamReader from '../messages/MStreamReader';
|
||||||
import JSONRawMessageReader from '../messages/JSONRawMessageReader'
|
import JSONRawMessageReader from '../messages/JSONRawMessageReader';
|
||||||
import Call, { CallingState } from './Call';
|
import Call, { CallingState } from './Call';
|
||||||
import RemoteControl, { RemoteControlStatus } from './RemoteControl'
|
import RemoteControl, { RemoteControlStatus } from './RemoteControl';
|
||||||
import ScreenRecording, { SessionRecordingStatus } from './ScreenRecording'
|
import ScreenRecording, { SessionRecordingStatus } from './ScreenRecording';
|
||||||
|
import CanvasReceiver from 'Player/web/assist/CanvasReceiver';
|
||||||
|
|
||||||
|
export { RemoteControlStatus, SessionRecordingStatus, CallingState };
|
||||||
export {
|
|
||||||
RemoteControlStatus,
|
|
||||||
SessionRecordingStatus,
|
|
||||||
CallingState,
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ConnectionStatus {
|
export enum ConnectionStatus {
|
||||||
Connecting,
|
Connecting,
|
||||||
|
|
@ -25,29 +22,30 @@ export enum ConnectionStatus {
|
||||||
Closed,
|
Closed,
|
||||||
}
|
}
|
||||||
|
|
||||||
type StatsEvent = 's_call_started'
|
type StatsEvent =
|
||||||
|
| 's_call_started'
|
||||||
| 's_call_ended'
|
| 's_call_ended'
|
||||||
| 's_control_started'
|
| 's_control_started'
|
||||||
| 's_control_ended'
|
| 's_control_ended'
|
||||||
| 's_recording_started'
|
| 's_recording_started'
|
||||||
| 's_recording_ended'
|
| 's_recording_ended';
|
||||||
|
|
||||||
export function getStatusText(status: ConnectionStatus): string {
|
export function getStatusText(status: ConnectionStatus): string {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case ConnectionStatus.Closed:
|
case ConnectionStatus.Closed:
|
||||||
return 'Closed...';
|
return 'Closed...';
|
||||||
case ConnectionStatus.Connecting:
|
case ConnectionStatus.Connecting:
|
||||||
return "Connecting...";
|
return 'Connecting...';
|
||||||
case ConnectionStatus.Connected:
|
case ConnectionStatus.Connected:
|
||||||
return "";
|
return '';
|
||||||
case ConnectionStatus.Inactive:
|
case ConnectionStatus.Inactive:
|
||||||
return "Client tab is inactive";
|
return 'Client tab is inactive';
|
||||||
case ConnectionStatus.Disconnected:
|
case ConnectionStatus.Disconnected:
|
||||||
return "Disconnected";
|
return 'Disconnected';
|
||||||
case ConnectionStatus.Error:
|
case ConnectionStatus.Error:
|
||||||
return "Something went wrong. Try to reload the page.";
|
return 'Something went wrong. Try to reload the page.';
|
||||||
case ConnectionStatus.WaitingMessages:
|
case ConnectionStatus.WaitingMessages:
|
||||||
return "Connected. Waiting for the data... (The tab might be inactive)"
|
return 'Connected. Waiting for the data... (The tab might be inactive)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,14 +57,16 @@ export function getStatusText(status: ConnectionStatus): string {
|
||||||
const MAX_RECONNECTION_COUNT = 4;
|
const MAX_RECONNECTION_COUNT = 4;
|
||||||
|
|
||||||
export default class AssistManager {
|
export default class AssistManager {
|
||||||
assistVersion = 1
|
assistVersion = 1;
|
||||||
|
private canvasReceiver: CanvasReceiver;
|
||||||
static readonly INITIAL_STATE = {
|
static readonly INITIAL_STATE = {
|
||||||
peerConnectionStatus: ConnectionStatus.Connecting,
|
peerConnectionStatus: ConnectionStatus.Connecting,
|
||||||
assistStart: 0,
|
assistStart: 0,
|
||||||
...Call.INITIAL_STATE,
|
...Call.INITIAL_STATE,
|
||||||
...RemoteControl.INITIAL_STATE,
|
...RemoteControl.INITIAL_STATE,
|
||||||
...ScreenRecording.INITIAL_STATE,
|
...ScreenRecording.INITIAL_STATE,
|
||||||
}
|
};
|
||||||
|
|
||||||
// TODO: Session type
|
// TODO: Session type
|
||||||
constructor(
|
constructor(
|
||||||
private session: any,
|
private session: any,
|
||||||
|
|
@ -75,26 +75,31 @@ export default class AssistManager {
|
||||||
private screen: Screen,
|
private screen: Screen,
|
||||||
private config: RTCIceServer[] | null,
|
private config: RTCIceServer[] | null,
|
||||||
private store: Store<typeof AssistManager.INITIAL_STATE>,
|
private store: Store<typeof AssistManager.INITIAL_STATE>,
|
||||||
public readonly uiErrorHandler?: { error: (msg: string) => void }
|
private getNode: MessageManager['getNode'],
|
||||||
|
public readonly uiErrorHandler?: {
|
||||||
|
error: (msg: string) => void;
|
||||||
|
}
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public getAssistVersion = () => this.assistVersion
|
public getAssistVersion = () => this.assistVersion;
|
||||||
|
|
||||||
private get borderStyle() {
|
private get borderStyle() {
|
||||||
const { recordingState, remoteControl } = this.store.get()
|
const { recordingState, remoteControl } = this.store.get();
|
||||||
|
|
||||||
const isRecordingActive = recordingState === SessionRecordingStatus.Recording
|
const isRecordingActive = recordingState === SessionRecordingStatus.Recording;
|
||||||
const isControlActive = remoteControl === RemoteControlStatus.Enabled
|
const isControlActive = remoteControl === RemoteControlStatus.Enabled;
|
||||||
// recording gets priority here
|
// recording gets priority here
|
||||||
if (isRecordingActive) return { outline: '2px dashed red' }
|
if (isRecordingActive) return { outline: '2px dashed red' };
|
||||||
if (isControlActive) return { outline: '2px dashed blue' }
|
if (isControlActive) return { outline: '2px dashed blue' };
|
||||||
return { outline: 'unset' }
|
return { outline: 'unset' };
|
||||||
}
|
}
|
||||||
|
|
||||||
private setStatus(status: ConnectionStatus) {
|
private setStatus(status: ConnectionStatus) {
|
||||||
if (this.store.get().peerConnectionStatus === ConnectionStatus.Disconnected &&
|
if (
|
||||||
status !== ConnectionStatus.Connected) {
|
this.store.get().peerConnectionStatus === ConnectionStatus.Disconnected &&
|
||||||
return
|
status !== ConnectionStatus.Connected
|
||||||
|
) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === ConnectionStatus.Connecting) {
|
if (status === ConnectionStatus.Connecting) {
|
||||||
|
|
@ -111,145 +116,154 @@ export default class AssistManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
private get peerID(): string {
|
private get peerID(): string {
|
||||||
return `${this.session.projectKey}-${this.session.sessionId}`
|
return `${this.session.projectKey}-${this.session.sessionId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private socketCloseTimeout: ReturnType<typeof setTimeout> | undefined
|
private socketCloseTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||||
private onVisChange = () => {
|
private onVisChange = () => {
|
||||||
this.socketCloseTimeout && clearTimeout(this.socketCloseTimeout)
|
this.socketCloseTimeout && clearTimeout(this.socketCloseTimeout);
|
||||||
if (document.hidden) {
|
if (document.hidden) {
|
||||||
this.socketCloseTimeout = setTimeout(() => {
|
this.socketCloseTimeout = setTimeout(() => {
|
||||||
const state = this.store.get()
|
const state = this.store.get();
|
||||||
if (document.hidden &&
|
if (
|
||||||
|
document.hidden &&
|
||||||
// TODO: should it be RemoteControlStatus.Disabled? (check)
|
// TODO: should it be RemoteControlStatus.Disabled? (check)
|
||||||
(state.calling === CallingState.NoCall && state.remoteControl === RemoteControlStatus.Enabled)) {
|
state.calling === CallingState.NoCall &&
|
||||||
this.socket?.close()
|
state.remoteControl === RemoteControlStatus.Enabled
|
||||||
|
) {
|
||||||
|
this.socket?.close();
|
||||||
}
|
}
|
||||||
}, 30000)
|
}, 30000);
|
||||||
} else {
|
} else {
|
||||||
this.socket?.open()
|
this.socket?.open();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private socket: Socket | null = null;
|
||||||
|
private disconnectTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
private inactiveTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
private inactiveTabs: string[] = [];
|
||||||
|
|
||||||
private socket: Socket | null = null
|
|
||||||
private disconnectTimeout: ReturnType<typeof setTimeout> | undefined
|
|
||||||
private inactiveTimeout: ReturnType<typeof setTimeout> | undefined
|
|
||||||
private inactiveTabs: string[] = []
|
|
||||||
private clearDisconnectTimeout() {
|
private clearDisconnectTimeout() {
|
||||||
this.disconnectTimeout && clearTimeout(this.disconnectTimeout)
|
this.disconnectTimeout && clearTimeout(this.disconnectTimeout);
|
||||||
this.disconnectTimeout = undefined
|
this.disconnectTimeout = undefined;
|
||||||
}
|
}
|
||||||
private clearInactiveTimeout() {
|
|
||||||
this.inactiveTimeout && clearTimeout(this.inactiveTimeout)
|
|
||||||
this.inactiveTimeout = undefined
|
|
||||||
}
|
|
||||||
connect(agentToken: string, agentId: number, projectId: number) {
|
|
||||||
const jmr = new JSONRawMessageReader()
|
|
||||||
const reader = new MStreamReader(jmr, this.session.startedAt)
|
|
||||||
let waitingForMessages = true
|
|
||||||
|
|
||||||
const now = +new Date()
|
private clearInactiveTimeout() {
|
||||||
this.store.update({ assistStart: now })
|
this.inactiveTimeout && clearTimeout(this.inactiveTimeout);
|
||||||
|
this.inactiveTimeout = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(agentToken: string, agentId: number, projectId: number) {
|
||||||
|
const jmr = new JSONRawMessageReader();
|
||||||
|
const reader = new MStreamReader(jmr, this.session.startedAt);
|
||||||
|
let waitingForMessages = true;
|
||||||
|
|
||||||
|
const now = +new Date();
|
||||||
|
this.store.update({ assistStart: now });
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import('socket.io-client').then(({ default: io }) => {
|
import('socket.io-client').then(({ default: io }) => {
|
||||||
if (this.socket != null || this.cleaned) { return }
|
if (this.socket != null || this.cleaned) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const urlObject = new URL(window.env.API_EDP || window.location.origin) // does it handle ssl automatically?
|
const urlObject = new URL(window.env.API_EDP || window.location.origin); // does it handle ssl automatically?
|
||||||
|
|
||||||
const socket: Socket = this.socket = io(urlObject.origin, {
|
const socket: Socket = (this.socket = io(urlObject.origin, {
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
multiplex: true,
|
multiplex: true,
|
||||||
transports: ['websocket'],
|
transports: ['websocket'],
|
||||||
path: '/ws-assist/socket',
|
path: '/ws-assist/socket',
|
||||||
auth: {
|
auth: {
|
||||||
token: agentToken
|
token: agentToken,
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
peerId: this.peerID,
|
peerId: this.peerID,
|
||||||
projectId,
|
projectId,
|
||||||
identity: "agent",
|
identity: 'agent',
|
||||||
agentInfo: JSON.stringify({
|
agentInfo: JSON.stringify({
|
||||||
...this.session.agentInfo,
|
...this.session.agentInfo,
|
||||||
id: agentId,
|
id: agentId,
|
||||||
|
peerId: this.peerID,
|
||||||
query: document.location.search,
|
query: document.location.search,
|
||||||
})
|
}),
|
||||||
}
|
},
|
||||||
})
|
}));
|
||||||
socket.on("connect", () => {
|
socket.on('connect', () => {
|
||||||
waitingForMessages = true
|
waitingForMessages = true;
|
||||||
this.setStatus(ConnectionStatus.WaitingMessages) // TODO: reconnect happens frequently on bad network
|
this.setStatus(ConnectionStatus.WaitingMessages); // TODO: reconnect happens frequently on bad network
|
||||||
})
|
});
|
||||||
|
|
||||||
socket.on('messages', messages => {
|
socket.on('messages', (messages) => {
|
||||||
const isOldVersion = messages.meta.version === 1
|
const isOldVersion = messages.meta.version === 1;
|
||||||
this.assistVersion = isOldVersion ? 1 : 2
|
this.assistVersion = isOldVersion ? 1 : 2;
|
||||||
|
|
||||||
const data = messages.data || messages
|
const data = messages.data || messages;
|
||||||
jmr.append(data) // as RawMessage[]
|
jmr.append(data); // as RawMessage[]
|
||||||
if (waitingForMessages) {
|
if (waitingForMessages) {
|
||||||
waitingForMessages = false // TODO: more explicit
|
waitingForMessages = false; // TODO: more explicit
|
||||||
this.setStatus(ConnectionStatus.Connected)
|
this.setStatus(ConnectionStatus.Connected);
|
||||||
}
|
}
|
||||||
if (messages.meta.tabId !== this.store.get().currentTab) {
|
if (messages.meta.tabId !== this.store.get().currentTab) {
|
||||||
this.clearDisconnectTimeout()
|
this.clearDisconnectTimeout();
|
||||||
if (isOldVersion) {
|
if (isOldVersion) {
|
||||||
reader.currentTab = messages.meta.tabId
|
reader.currentTab = messages.meta.tabId;
|
||||||
this.store.update({ currentTab: messages.meta.tabId })
|
this.store.update({ currentTab: messages.meta.tabId });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let msg = reader.readNext(); msg !== null; msg = reader.readNext()) {
|
for (let msg = reader.readNext(); msg !== null; msg = reader.readNext()) {
|
||||||
this.handleMessage(msg, msg._index)
|
this.handleMessage(msg, msg._index);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
socket.on('SESSION_RECONNECTED', () => {
|
socket.on('SESSION_RECONNECTED', () => {
|
||||||
this.clearDisconnectTimeout()
|
this.clearDisconnectTimeout();
|
||||||
this.clearInactiveTimeout()
|
this.clearInactiveTimeout();
|
||||||
this.setStatus(ConnectionStatus.Connected)
|
this.setStatus(ConnectionStatus.Connected);
|
||||||
})
|
});
|
||||||
|
|
||||||
socket.on('UPDATE_SESSION', (evData) => {
|
socket.on('UPDATE_SESSION', (evData) => {
|
||||||
const { meta = {}, data = {} } = evData
|
const { meta = {}, data = {} } = evData;
|
||||||
const { tabId } = meta
|
const { tabId } = meta;
|
||||||
const usedData = this.assistVersion === 1 ? evData : data
|
const usedData = this.assistVersion === 1 ? evData : data;
|
||||||
const { active } = usedData
|
const { active } = usedData;
|
||||||
const currentTab = this.store.get().currentTab
|
const currentTab = this.store.get().currentTab;
|
||||||
this.clearDisconnectTimeout()
|
this.clearDisconnectTimeout();
|
||||||
!this.inactiveTimeout && this.setStatus(ConnectionStatus.Connected)
|
!this.inactiveTimeout && this.setStatus(ConnectionStatus.Connected);
|
||||||
if (typeof active === "boolean") {
|
if (typeof active === 'boolean') {
|
||||||
this.clearInactiveTimeout()
|
this.clearInactiveTimeout();
|
||||||
if (active) {
|
if (active) {
|
||||||
this.setStatus(ConnectionStatus.Connected)
|
this.setStatus(ConnectionStatus.Connected);
|
||||||
this.inactiveTabs = this.inactiveTabs.filter(t => t !== tabId)
|
this.inactiveTabs = this.inactiveTabs.filter((t) => t !== tabId);
|
||||||
} else {
|
} else {
|
||||||
if (!this.inactiveTabs.includes(tabId)) {
|
if (!this.inactiveTabs.includes(tabId)) {
|
||||||
this.inactiveTabs.push(tabId)
|
this.inactiveTabs.push(tabId);
|
||||||
}
|
}
|
||||||
if (tabId === undefined || tabId === currentTab) {
|
if (tabId === undefined || tabId === currentTab) {
|
||||||
this.inactiveTimeout = setTimeout(() => {
|
this.inactiveTimeout = setTimeout(() => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const tabs = this.store.get().tabs
|
const tabs = this.store.get().tabs;
|
||||||
if (this.inactiveTabs.length === tabs.size) {
|
if (this.inactiveTabs.length === tabs.size) {
|
||||||
this.setStatus(ConnectionStatus.Inactive)
|
this.setStatus(ConnectionStatus.Inactive);
|
||||||
}
|
}
|
||||||
}, 10000)
|
}, 10000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
socket.on('SESSION_DISCONNECTED', e => {
|
socket.on('SESSION_DISCONNECTED', (e) => {
|
||||||
waitingForMessages = true
|
waitingForMessages = true;
|
||||||
this.clearDisconnectTimeout()
|
this.clearDisconnectTimeout();
|
||||||
this.disconnectTimeout = setTimeout(() => {
|
this.disconnectTimeout = setTimeout(() => {
|
||||||
this.setStatus(ConnectionStatus.Disconnected)
|
this.setStatus(ConnectionStatus.Disconnected);
|
||||||
}, 30000)
|
}, 30000);
|
||||||
})
|
});
|
||||||
socket.on('error', e => {
|
socket.on('error', (e) => {
|
||||||
console.warn("Socket error: ", e )
|
console.warn('Socket error: ', e);
|
||||||
this.setStatus(ConnectionStatus.Error);
|
this.setStatus(ConnectionStatus.Error);
|
||||||
})
|
});
|
||||||
|
|
||||||
// Maybe do lazy initialization for all?
|
// Maybe do lazy initialization for all?
|
||||||
// TODO: socket proxy (depend on interfaces)
|
// TODO: socket proxy (depend on interfaces)
|
||||||
|
|
@ -259,88 +273,93 @@ export default class AssistManager {
|
||||||
this.config,
|
this.config,
|
||||||
this.peerID,
|
this.peerID,
|
||||||
this.getAssistVersion
|
this.getAssistVersion
|
||||||
)
|
);
|
||||||
this.remoteControl = new RemoteControl(
|
this.remoteControl = new RemoteControl(
|
||||||
this.store,
|
this.store,
|
||||||
socket,
|
socket,
|
||||||
this.screen,
|
this.screen,
|
||||||
this.session.agentInfo,
|
this.session.agentInfo,
|
||||||
() => this.screen.setBorderStyle(this.borderStyle),
|
() => this.screen.setBorderStyle(this.borderStyle),
|
||||||
this.getAssistVersion,
|
this.getAssistVersion
|
||||||
)
|
);
|
||||||
this.screenRecording = new ScreenRecording(
|
this.screenRecording = new ScreenRecording(
|
||||||
this.store,
|
this.store,
|
||||||
socket,
|
socket,
|
||||||
this.session.agentInfo,
|
this.session.agentInfo,
|
||||||
() => this.screen.setBorderStyle(this.borderStyle),
|
() => this.screen.setBorderStyle(this.borderStyle),
|
||||||
this.uiErrorHandler,
|
this.uiErrorHandler,
|
||||||
this.getAssistVersion,
|
this.getAssistVersion
|
||||||
)
|
);
|
||||||
|
this.canvasReceiver = new CanvasReceiver(this.peerID, this.config, this.getNode, {
|
||||||
|
...this.session.agentInfo,
|
||||||
|
id: agentId,
|
||||||
|
});
|
||||||
|
|
||||||
document.addEventListener('visibilitychange', this.onVisChange)
|
document.addEventListener('visibilitychange', this.onVisChange);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public ping(event: StatsEvent, id: number) {
|
public ping(event: StatsEvent, id: number) {
|
||||||
this.socket?.emit(event, id)
|
this.socket?.emit(event, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ==== ScreenRecording ==== */
|
/* ==== ScreenRecording ==== */
|
||||||
private screenRecording: ScreenRecording | null = null
|
private screenRecording: ScreenRecording | null = null;
|
||||||
requestRecording = (...args: Parameters<ScreenRecording['requestRecording']>) => {
|
requestRecording = (...args: Parameters<ScreenRecording['requestRecording']>) => {
|
||||||
return this.screenRecording?.requestRecording(...args)
|
return this.screenRecording?.requestRecording(...args);
|
||||||
}
|
};
|
||||||
stopRecording = (...args: Parameters<ScreenRecording['stopRecording']>) => {
|
stopRecording = (...args: Parameters<ScreenRecording['stopRecording']>) => {
|
||||||
return this.screenRecording?.stopRecording(...args)
|
return this.screenRecording?.stopRecording(...args);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
/* ==== RemoteControl ==== */
|
/* ==== RemoteControl ==== */
|
||||||
private remoteControl: RemoteControl | null = null
|
private remoteControl: RemoteControl | null = null;
|
||||||
requestReleaseRemoteControl = (...args: Parameters<RemoteControl['requestReleaseRemoteControl']>) => {
|
requestReleaseRemoteControl = (
|
||||||
return this.remoteControl?.requestReleaseRemoteControl(...args)
|
...args: Parameters<RemoteControl['requestReleaseRemoteControl']>
|
||||||
}
|
) => {
|
||||||
|
return this.remoteControl?.requestReleaseRemoteControl(...args);
|
||||||
|
};
|
||||||
setRemoteControlCallbacks = (...args: Parameters<RemoteControl['setCallbacks']>) => {
|
setRemoteControlCallbacks = (...args: Parameters<RemoteControl['setCallbacks']>) => {
|
||||||
return this.remoteControl?.setCallbacks(...args)
|
return this.remoteControl?.setCallbacks(...args);
|
||||||
}
|
};
|
||||||
releaseRemoteControl = (...args: Parameters<RemoteControl['releaseRemoteControl']>) => {
|
releaseRemoteControl = (...args: Parameters<RemoteControl['releaseRemoteControl']>) => {
|
||||||
return this.remoteControl?.releaseRemoteControl(...args)
|
return this.remoteControl?.releaseRemoteControl(...args);
|
||||||
}
|
};
|
||||||
toggleAnnotation = (...args: Parameters<RemoteControl['toggleAnnotation']>) => {
|
toggleAnnotation = (...args: Parameters<RemoteControl['toggleAnnotation']>) => {
|
||||||
return this.remoteControl?.toggleAnnotation(...args)
|
return this.remoteControl?.toggleAnnotation(...args);
|
||||||
}
|
};
|
||||||
|
|
||||||
/* ==== Call ==== */
|
/* ==== Call ==== */
|
||||||
private callManager: Call | null = null
|
private callManager: Call | null = null;
|
||||||
initiateCallEnd = async (...args: Parameters<Call['initiateCallEnd']>) => {
|
initiateCallEnd = async (...args: Parameters<Call['initiateCallEnd']>) => {
|
||||||
return this.callManager?.initiateCallEnd(...args)
|
return this.callManager?.initiateCallEnd(...args);
|
||||||
}
|
};
|
||||||
setCallArgs = (...args: Parameters<Call['setCallArgs']>) => {
|
setCallArgs = (...args: Parameters<Call['setCallArgs']>) => {
|
||||||
return this.callManager?.setCallArgs(...args)
|
return this.callManager?.setCallArgs(...args);
|
||||||
}
|
};
|
||||||
call = (...args: Parameters<Call['call']>) => {
|
call = (...args: Parameters<Call['call']>) => {
|
||||||
return this.callManager?.call(...args)
|
return this.callManager?.call(...args);
|
||||||
}
|
};
|
||||||
toggleVideoLocalStream = (...args: Parameters<Call['toggleVideoLocalStream']>) => {
|
toggleVideoLocalStream = (...args: Parameters<Call['toggleVideoLocalStream']>) => {
|
||||||
return this.callManager?.toggleVideoLocalStream(...args)
|
return this.callManager?.toggleVideoLocalStream(...args);
|
||||||
}
|
};
|
||||||
addPeerCall = (...args: Parameters<Call['addPeerCall']>) => {
|
addPeerCall = (...args: Parameters<Call['addPeerCall']>) => {
|
||||||
return this.callManager?.addPeerCall(...args)
|
return this.callManager?.addPeerCall(...args);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
/* ==== Cleaning ==== */
|
/* ==== Cleaning ==== */
|
||||||
private cleaned = false
|
private cleaned = false;
|
||||||
|
|
||||||
clean() {
|
clean() {
|
||||||
this.cleaned = true // sometimes cleaned before modules loaded
|
this.cleaned = true; // sometimes cleaned before modules loaded
|
||||||
this.remoteControl?.clean()
|
this.remoteControl?.clean();
|
||||||
this.callManager?.clean()
|
this.callManager?.clean();
|
||||||
this.socket?.close()
|
this.canvasReceiver?.clear();
|
||||||
this.socket = null
|
this.socket?.close();
|
||||||
this.clearDisconnectTimeout()
|
this.socket = null;
|
||||||
this.clearInactiveTimeout()
|
this.clearDisconnectTimeout();
|
||||||
this.socketCloseTimeout && clearTimeout(this.socketCloseTimeout)
|
this.clearInactiveTimeout();
|
||||||
document.removeEventListener('visibilitychange', this.onVisChange)
|
this.socketCloseTimeout && clearTimeout(this.socketCloseTimeout);
|
||||||
|
document.removeEventListener('visibilitychange', this.onVisChange);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ export default class Call {
|
||||||
private videoStreams: Record<string, MediaStreamTrack> = {};
|
private videoStreams: Record<string, MediaStreamTrack> = {};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private store: Store<State>,
|
private store: Store<State & { tabs: Set<string> }>,
|
||||||
private socket: Socket,
|
private socket: Socket,
|
||||||
private config: RTCIceServer[] | null,
|
private config: RTCIceServer[] | null,
|
||||||
private peerID: string,
|
private peerID: string,
|
||||||
|
|
@ -253,7 +253,7 @@ export default class Call {
|
||||||
this.getAssistVersion() === 1
|
this.getAssistVersion() === 1
|
||||||
? this.peerID
|
? this.peerID
|
||||||
: `${this.peerID}-${tab || Object.keys(this.store.get().tabs)[0]}`;
|
: `${this.peerID}-${tab || Object.keys(this.store.get().tabs)[0]}`;
|
||||||
console.log(peerId, this.getAssistVersion());
|
|
||||||
void this._peerConnection(peerId);
|
void this._peerConnection(peerId);
|
||||||
this.emitData('_agent_name', appStore.getState().getIn(['user', 'account', 'name']));
|
this.emitData('_agent_name', appStore.getState().getIn(['user', 'account', 'name']));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
162
frontend/app/player/web/assist/CanvasReceiver.ts
Normal file
162
frontend/app/player/web/assist/CanvasReceiver.ts
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
import Peer from 'peerjs';
|
||||||
|
import { VElement } from 'Player/web/managers/DOM/VirtualDOM';
|
||||||
|
import MessageManager from 'Player/web/MessageManager';
|
||||||
|
|
||||||
|
let frameCounter = 0;
|
||||||
|
|
||||||
|
function draw(
|
||||||
|
video: HTMLVideoElement,
|
||||||
|
canvas: HTMLCanvasElement,
|
||||||
|
canvasCtx: CanvasRenderingContext2D
|
||||||
|
) {
|
||||||
|
if (frameCounter % 4 === 0) {
|
||||||
|
canvasCtx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||||
|
}
|
||||||
|
frameCounter++;
|
||||||
|
requestAnimationFrame(() => draw(video, canvas, canvasCtx));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class CanvasReceiver {
|
||||||
|
private streams: Map<string, MediaStream> = new Map();
|
||||||
|
private peer: Peer | null = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly peerIdPrefix: string,
|
||||||
|
private readonly config: RTCIceServer[] | null,
|
||||||
|
private readonly getNode: MessageManager['getNode'],
|
||||||
|
private readonly agentInfo: Record<string, any>
|
||||||
|
) {
|
||||||
|
// @ts-ignore
|
||||||
|
const urlObject = new URL(window.env.API_EDP || window.location.origin);
|
||||||
|
const peerOpts: Peer.PeerJSOption = {
|
||||||
|
host: urlObject.hostname,
|
||||||
|
path: '/assist',
|
||||||
|
port:
|
||||||
|
urlObject.port === ''
|
||||||
|
? location.protocol === 'https:'
|
||||||
|
? 443
|
||||||
|
: 80
|
||||||
|
: parseInt(urlObject.port),
|
||||||
|
};
|
||||||
|
if (this.config) {
|
||||||
|
peerOpts['config'] = {
|
||||||
|
iceServers: this.config,
|
||||||
|
//@ts-ignore
|
||||||
|
sdpSemantics: 'unified-plan',
|
||||||
|
iceTransportPolicy: 'all',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const id = `${this.peerIdPrefix}-${this.agentInfo.id}-canvas`;
|
||||||
|
const canvasPeer = new Peer(id, peerOpts);
|
||||||
|
this.peer = canvasPeer;
|
||||||
|
canvasPeer.on('error', (err) => console.error('canvas peer error', err));
|
||||||
|
canvasPeer.on('call', (call) => {
|
||||||
|
call.answer();
|
||||||
|
const canvasId = call.peer.split('-')[2];
|
||||||
|
call.on('stream', (stream) => {
|
||||||
|
this.streams.set(canvasId, stream);
|
||||||
|
setTimeout(() => {
|
||||||
|
const node = this.getNode(parseInt(canvasId, 10));
|
||||||
|
const videoEl = spawnVideo(
|
||||||
|
this.streams.get(canvasId)?.clone() as MediaStream,
|
||||||
|
node as VElement
|
||||||
|
);
|
||||||
|
if (node) {
|
||||||
|
draw(
|
||||||
|
videoEl,
|
||||||
|
node.node as HTMLCanvasElement,
|
||||||
|
(node.node as HTMLCanvasElement).getContext('2d') as CanvasRenderingContext2D
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
call.on('error', (err) => console.error('canvas call error', err));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
if (this.peer) {
|
||||||
|
// otherwise it calls reconnection on data chan close
|
||||||
|
const peer = this.peer;
|
||||||
|
this.peer = null;
|
||||||
|
peer.disconnect();
|
||||||
|
peer.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function spawnVideo(stream: MediaStream, node: VElement) {
|
||||||
|
const videoEl = document.createElement('video');
|
||||||
|
|
||||||
|
videoEl.srcObject = stream
|
||||||
|
videoEl.setAttribute('autoplay', 'true');
|
||||||
|
videoEl.setAttribute('muted', 'true');
|
||||||
|
videoEl.setAttribute('playsinline', 'true');
|
||||||
|
videoEl.setAttribute('crossorigin', 'anonymous');
|
||||||
|
void videoEl.play();
|
||||||
|
|
||||||
|
return videoEl;
|
||||||
|
}
|
||||||
|
|
||||||
|
function spawnDebugVideo(stream: MediaStream, node: VElement) {
|
||||||
|
const video = document.createElement('video');
|
||||||
|
video.id = 'canvas-or-testing';
|
||||||
|
video.style.border = '1px solid red';
|
||||||
|
video.setAttribute('autoplay', 'true');
|
||||||
|
video.setAttribute('muted', 'true');
|
||||||
|
video.setAttribute('playsinline', 'true');
|
||||||
|
video.setAttribute('crossorigin', 'anonymous');
|
||||||
|
|
||||||
|
const coords = node.node.getBoundingClientRect();
|
||||||
|
|
||||||
|
Object.assign(video.style, {
|
||||||
|
position: 'absolute',
|
||||||
|
left: `${coords.left}px`,
|
||||||
|
top: `${coords.top}px`,
|
||||||
|
width: `${coords.width}px`,
|
||||||
|
height: `${coords.height}px`,
|
||||||
|
});
|
||||||
|
video.width = coords.width;
|
||||||
|
video.height = coords.height;
|
||||||
|
video.srcObject = stream;
|
||||||
|
|
||||||
|
document.body.appendChild(video);
|
||||||
|
video
|
||||||
|
.play()
|
||||||
|
.then(() => {
|
||||||
|
console.log('started streaming canvas');
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
const waiter = () => {
|
||||||
|
void video.play();
|
||||||
|
document.removeEventListener('click', waiter);
|
||||||
|
};
|
||||||
|
document.addEventListener('click', waiter);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** simple peer example
|
||||||
|
* // @ts-ignore
|
||||||
|
* const peer = new SLPeer({ initiator: false })
|
||||||
|
* socket.on('c_signal', ({ data }) => {
|
||||||
|
* console.log('got signal', data)
|
||||||
|
* peer.signal(data.data);
|
||||||
|
* peer.canvasId = data.id;
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* peer.on('signal', (data: any) => {
|
||||||
|
* socket.emit('c_signal', data);
|
||||||
|
* });
|
||||||
|
* peer.on('stream', (stream: MediaStream) => {
|
||||||
|
* console.log('stream ready', stream, peer.canvasId);
|
||||||
|
* this.streams.set(peer.canvasId, stream)
|
||||||
|
* setTimeout(() => {
|
||||||
|
* const node = this.getNode(peer.canvasId)
|
||||||
|
* console.log(peer.canvasId, this.streams, node)
|
||||||
|
* spawnVideo(this.streams.get(peer.canvasId)?.clone(), node, this.screen)
|
||||||
|
* }, 500)
|
||||||
|
* })
|
||||||
|
* peer.on('error', console.error)
|
||||||
|
*
|
||||||
|
* */
|
||||||
53
frontend/app/player/web/managers/CanvasManager.ts
Normal file
53
frontend/app/player/web/managers/CanvasManager.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { VElement } from "Player/web/managers/DOM/VirtualDOM";
|
||||||
|
|
||||||
|
export default class CanvasManager {
|
||||||
|
private fileData: string | undefined;
|
||||||
|
private canvasEl: HTMLVideoElement
|
||||||
|
private canvasCtx: CanvasRenderingContext2D | null = null;
|
||||||
|
private videoTag = document.createElement('video')
|
||||||
|
private lastTs = 0;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly nodeId: string,
|
||||||
|
private readonly delta: number,
|
||||||
|
private readonly filename: string,
|
||||||
|
private readonly getNode: (id: number) => VElement | undefined) {
|
||||||
|
// getting mp4 file composed from canvas snapshot images
|
||||||
|
fetch(this.filename).then((r) => {
|
||||||
|
if (r.status === 200) {
|
||||||
|
r.blob().then((blob) => {
|
||||||
|
this.fileData = URL.createObjectURL(blob);
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return Promise.reject(`File ${this.filename} not found`)
|
||||||
|
}
|
||||||
|
}).catch(console.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
startVideo = () => {
|
||||||
|
if (!this.fileData) return;
|
||||||
|
this.videoTag.setAttribute('autoplay', 'true');
|
||||||
|
this.videoTag.setAttribute('muted', 'true');
|
||||||
|
this.videoTag.setAttribute('playsinline', 'true');
|
||||||
|
this.videoTag.setAttribute('crossorigin', 'anonymous');
|
||||||
|
this.videoTag.src = this.fileData;
|
||||||
|
this.videoTag.currentTime = 0;
|
||||||
|
|
||||||
|
const node = this.getNode(parseInt(this.nodeId, 10)) as unknown as VElement
|
||||||
|
this.canvasCtx = (node.node as HTMLCanvasElement).getContext('2d');
|
||||||
|
this.canvasEl = node.node as HTMLVideoElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
move(t: number) {
|
||||||
|
if (t - this.lastTs < 100) return;
|
||||||
|
this.lastTs = t;
|
||||||
|
const playTime = t - this.delta
|
||||||
|
if (playTime > 0) {
|
||||||
|
if (!this.videoTag.paused) {
|
||||||
|
void this.videoTag.pause()
|
||||||
|
}
|
||||||
|
this.videoTag.currentTime = playTime/1000;
|
||||||
|
this.canvasCtx?.drawImage(this.videoTag, 0, 0, this.canvasEl.width, this.canvasEl.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -113,6 +113,10 @@ export default class DOMManager extends ListWalker<Message> {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getNode(id: number) {
|
||||||
|
return this.vElements.get(id) || this.vTexts.get(id)
|
||||||
|
}
|
||||||
|
|
||||||
private insertNode({ parentID, id, index }: { parentID: number, id: number, index: number }): void {
|
private insertNode({ parentID, id, index }: { parentID: number, id: number, index: number }): void {
|
||||||
const child = this.vElements.get(id) || this.vTexts.get(id)
|
const child = this.vElements.get(id) || this.vTexts.get(id)
|
||||||
if (!child) {
|
if (!child) {
|
||||||
|
|
@ -208,7 +212,8 @@ export default class DOMManager extends ListWalker<Message> {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case MType.CreateElementNode: {
|
case MType.CreateElementNode: {
|
||||||
const vElem = new VElement(msg.tag, msg.svg)
|
// if (msg.tag.toLowerCase() === 'canvas') msg.tag = 'video'
|
||||||
|
const vElem = new VElement(msg.tag, msg.svg, msg.index)
|
||||||
if (['STYLE', 'style', 'LINK'].includes(msg.tag)) {
|
if (['STYLE', 'style', 'LINK'].includes(msg.tag)) {
|
||||||
vElem.prioritized = true
|
vElem.prioritized = true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,7 @@ export class VElement extends VParent<Element> {
|
||||||
parentNode: VParent | null = null /** Should be modified only by he parent itself */
|
parentNode: VParent | null = null /** Should be modified only by he parent itself */
|
||||||
private newAttributes: Map<string, string | false> = new Map()
|
private newAttributes: Map<string, string | false> = new Map()
|
||||||
|
|
||||||
constructor(readonly tagName: string, readonly isSVG = false) { super() }
|
constructor(readonly tagName: string, readonly isSVG = false, public readonly index: number) { super() }
|
||||||
protected createNode() {
|
protected createNode() {
|
||||||
try {
|
try {
|
||||||
return this.isSVG
|
return this.isSVG
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,10 @@ export default class PagesManager extends ListWalker<DOMManager> {
|
||||||
this.forEach(page => page.sort(comparator))
|
this.forEach(page => page.sort(comparator))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getNode(id: number) {
|
||||||
|
return this.currentPage?.getNode(id)
|
||||||
|
}
|
||||||
|
|
||||||
moveReady(t: number): Promise<void> {
|
moveReady(t: number): Promise<void> {
|
||||||
const requiredPage = this.moveGetLast(t)
|
const requiredPage = this.moveGetLast(t)
|
||||||
if (requiredPage != null) {
|
if (requiredPage != null) {
|
||||||
|
|
|
||||||
|
|
@ -713,6 +713,16 @@ export default class RawMessageReader extends PrimitiveReader {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 119: {
|
||||||
|
const nodeId = this.readString(); if (nodeId === null) { return resetPointer() }
|
||||||
|
const timestamp = this.readUint(); if (timestamp === null) { return resetPointer() }
|
||||||
|
return {
|
||||||
|
tp: MType.CanvasNode,
|
||||||
|
nodeId,
|
||||||
|
timestamp,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
case 93: {
|
case 93: {
|
||||||
const timestamp = this.readUint(); if (timestamp === null) { return resetPointer() }
|
const timestamp = this.readUint(); if (timestamp === null) { return resetPointer() }
|
||||||
const length = this.readUint(); if (length === null) { return resetPointer() }
|
const length = this.readUint(); if (length === null) { return resetPointer() }
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
import { MType } from './raw.gen'
|
import { MType } from './raw.gen'
|
||||||
|
|
||||||
const IOS_TYPES = [90,91,92,93,94,95,96,97,98,100,101,102,103,104,105,106,107,110,111]
|
const IOS_TYPES = [90,91,92,93,94,95,96,97,98,100,101,102,103,104,105,106,107,110,111]
|
||||||
const DOM_TYPES = [0,4,5,6,7,8,9,10,11,12,13,14,15,16,18,19,20,37,38,49,50,51,54,55,57,58,59,60,61,67,69,70,71,72,73,74,75,76,77,113,114,117,118]
|
const DOM_TYPES = [0,4,5,6,7,8,9,10,11,12,13,14,15,16,18,19,20,37,38,49,50,51,54,55,57,58,59,60,61,67,69,70,71,72,73,74,75,76,77,113,114,117,118,119]
|
||||||
export function isDOMType(t: MType) {
|
export function isDOMType(t: MType) {
|
||||||
return DOM_TYPES.includes(t)
|
return DOM_TYPES.includes(t)
|
||||||
}
|
}
|
||||||
|
|
@ -61,6 +61,7 @@ import type {
|
||||||
RawResourceTiming,
|
RawResourceTiming,
|
||||||
RawTabChange,
|
RawTabChange,
|
||||||
RawTabData,
|
RawTabData,
|
||||||
|
RawCanvasNode,
|
||||||
RawIosEvent,
|
RawIosEvent,
|
||||||
RawIosScreenChanges,
|
RawIosScreenChanges,
|
||||||
RawIosClickEvent,
|
RawIosClickEvent,
|
||||||
|
|
@ -190,6 +191,8 @@ export type TabChange = RawTabChange & Timed
|
||||||
|
|
||||||
export type TabData = RawTabData & Timed
|
export type TabData = RawTabData & Timed
|
||||||
|
|
||||||
|
export type CanvasNode = RawCanvasNode & Timed
|
||||||
|
|
||||||
export type IosEvent = RawIosEvent & Timed
|
export type IosEvent = RawIosEvent & Timed
|
||||||
|
|
||||||
export type IosScreenChanges = RawIosScreenChanges & Timed
|
export type IosScreenChanges = RawIosScreenChanges & Timed
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ export const enum MType {
|
||||||
ResourceTiming = 116,
|
ResourceTiming = 116,
|
||||||
TabChange = 117,
|
TabChange = 117,
|
||||||
TabData = 118,
|
TabData = 118,
|
||||||
|
CanvasNode = 119,
|
||||||
IosEvent = 93,
|
IosEvent = 93,
|
||||||
IosScreenChanges = 96,
|
IosScreenChanges = 96,
|
||||||
IosClickEvent = 100,
|
IosClickEvent = 100,
|
||||||
|
|
@ -476,6 +477,12 @@ export interface RawTabData {
|
||||||
tabId: string,
|
tabId: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RawCanvasNode {
|
||||||
|
tp: MType.CanvasNode,
|
||||||
|
nodeId: string,
|
||||||
|
timestamp: number,
|
||||||
|
}
|
||||||
|
|
||||||
export interface RawIosEvent {
|
export interface RawIosEvent {
|
||||||
tp: MType.IosEvent,
|
tp: MType.IosEvent,
|
||||||
timestamp: number,
|
timestamp: number,
|
||||||
|
|
@ -568,4 +575,4 @@ export interface RawIosIssueEvent {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export type RawMessage = RawTimestamp | RawSetPageLocation | RawSetViewportSize | RawSetViewportScroll | RawCreateDocument | RawCreateElementNode | RawCreateTextNode | RawMoveNode | RawRemoveNode | RawSetNodeAttribute | RawRemoveNodeAttribute | RawSetNodeData | RawSetCssData | RawSetNodeScroll | RawSetInputValue | RawSetInputChecked | RawMouseMove | RawNetworkRequestDeprecated | RawConsoleLog | RawCssInsertRule | RawCssDeleteRule | RawFetch | RawProfiler | RawOTable | RawRedux | RawVuex | RawMobX | RawNgRx | RawGraphQl | RawPerformanceTrack | RawStringDict | RawSetNodeAttributeDict | RawResourceTimingDeprecated | RawConnectionInformation | RawSetPageVisibility | RawLoadFontFace | RawSetNodeFocus | RawLongTask | RawSetNodeAttributeURLBased | RawSetCssDataURLBased | RawCssInsertRuleURLBased | RawMouseClick | RawCreateIFrameDocument | RawAdoptedSsReplaceURLBased | RawAdoptedSsReplace | RawAdoptedSsInsertRuleURLBased | RawAdoptedSsInsertRule | RawAdoptedSsDeleteRule | RawAdoptedSsAddOwner | RawAdoptedSsRemoveOwner | RawZustand | RawNetworkRequest | RawSelectionChange | RawMouseThrashing | RawResourceTiming | RawTabChange | RawTabData | RawIosEvent | RawIosScreenChanges | RawIosClickEvent | RawIosInputEvent | RawIosPerformanceEvent | RawIosLog | RawIosInternalError | RawIosNetworkCall | RawIosSwipeEvent | RawIosIssueEvent;
|
export type RawMessage = RawTimestamp | RawSetPageLocation | RawSetViewportSize | RawSetViewportScroll | RawCreateDocument | RawCreateElementNode | RawCreateTextNode | RawMoveNode | RawRemoveNode | RawSetNodeAttribute | RawRemoveNodeAttribute | RawSetNodeData | RawSetCssData | RawSetNodeScroll | RawSetInputValue | RawSetInputChecked | RawMouseMove | RawNetworkRequestDeprecated | RawConsoleLog | RawCssInsertRule | RawCssDeleteRule | RawFetch | RawProfiler | RawOTable | RawRedux | RawVuex | RawMobX | RawNgRx | RawGraphQl | RawPerformanceTrack | RawStringDict | RawSetNodeAttributeDict | RawResourceTimingDeprecated | RawConnectionInformation | RawSetPageVisibility | RawLoadFontFace | RawSetNodeFocus | RawLongTask | RawSetNodeAttributeURLBased | RawSetCssDataURLBased | RawCssInsertRuleURLBased | RawMouseClick | RawCreateIFrameDocument | RawAdoptedSsReplaceURLBased | RawAdoptedSsReplace | RawAdoptedSsInsertRuleURLBased | RawAdoptedSsInsertRule | RawAdoptedSsDeleteRule | RawAdoptedSsAddOwner | RawAdoptedSsRemoveOwner | RawZustand | RawNetworkRequest | RawSelectionChange | RawMouseThrashing | RawResourceTiming | RawTabChange | RawTabData | RawCanvasNode | RawIosEvent | RawIosScreenChanges | RawIosClickEvent | RawIosInputEvent | RawIosPerformanceEvent | RawIosLog | RawIosInternalError | RawIosNetworkCall | RawIosSwipeEvent | RawIosIssueEvent;
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ export const TP_MAP = {
|
||||||
116: MType.ResourceTiming,
|
116: MType.ResourceTiming,
|
||||||
117: MType.TabChange,
|
117: MType.TabChange,
|
||||||
118: MType.TabData,
|
118: MType.TabData,
|
||||||
|
119: MType.CanvasNode,
|
||||||
93: MType.IosEvent,
|
93: MType.IosEvent,
|
||||||
96: MType.IosScreenChanges,
|
96: MType.IosScreenChanges,
|
||||||
100: MType.IosClickEvent,
|
100: MType.IosClickEvent,
|
||||||
|
|
|
||||||
|
|
@ -493,8 +493,14 @@ type TrTabData = [
|
||||||
tabId: string,
|
tabId: string,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
type TrCanvasNode = [
|
||||||
|
type: 119,
|
||||||
|
nodeId: string,
|
||||||
|
timestamp: number,
|
||||||
|
]
|
||||||
|
|
||||||
export type TrackerMessage = TrTimestamp | TrSetPageLocation | TrSetViewportSize | TrSetViewportScroll | TrCreateDocument | TrCreateElementNode | TrCreateTextNode | TrMoveNode | TrRemoveNode | TrSetNodeAttribute | TrRemoveNodeAttribute | TrSetNodeData | TrSetNodeScroll | TrSetInputTarget | TrSetInputValue | TrSetInputChecked | TrMouseMove | TrNetworkRequestDeprecated | TrConsoleLog | TrPageLoadTiming | TrPageRenderTiming | TrCustomEvent | TrUserID | TrUserAnonymousID | TrMetadata | TrCSSInsertRule | TrCSSDeleteRule | TrFetch | TrProfiler | TrOTable | TrStateAction | TrRedux | TrVuex | TrMobX | TrNgRx | TrGraphQL | TrPerformanceTrack | TrStringDict | TrSetNodeAttributeDict | TrResourceTimingDeprecated | TrConnectionInformation | TrSetPageVisibility | TrLoadFontFace | TrSetNodeFocus | TrLongTask | TrSetNodeAttributeURLBased | TrSetCSSDataURLBased | TrTechnicalInfo | TrCustomIssue | TrCSSInsertRuleURLBased | TrMouseClick | TrCreateIFrameDocument | TrAdoptedSSReplaceURLBased | TrAdoptedSSInsertRuleURLBased | TrAdoptedSSDeleteRule | TrAdoptedSSAddOwner | TrAdoptedSSRemoveOwner | TrJSException | TrZustand | TrBatchMetadata | TrPartitionedMessage | TrNetworkRequest | TrInputChange | TrSelectionChange | TrMouseThrashing | TrUnbindNodes | TrResourceTiming | TrTabChange | TrTabData
|
|
||||||
|
export type TrackerMessage = TrTimestamp | TrSetPageLocation | TrSetViewportSize | TrSetViewportScroll | TrCreateDocument | TrCreateElementNode | TrCreateTextNode | TrMoveNode | TrRemoveNode | TrSetNodeAttribute | TrRemoveNodeAttribute | TrSetNodeData | TrSetNodeScroll | TrSetInputTarget | TrSetInputValue | TrSetInputChecked | TrMouseMove | TrNetworkRequestDeprecated | TrConsoleLog | TrPageLoadTiming | TrPageRenderTiming | TrCustomEvent | TrUserID | TrUserAnonymousID | TrMetadata | TrCSSInsertRule | TrCSSDeleteRule | TrFetch | TrProfiler | TrOTable | TrStateAction | TrRedux | TrVuex | TrMobX | TrNgRx | TrGraphQL | TrPerformanceTrack | TrStringDict | TrSetNodeAttributeDict | TrResourceTimingDeprecated | TrConnectionInformation | TrSetPageVisibility | TrLoadFontFace | TrSetNodeFocus | TrLongTask | TrSetNodeAttributeURLBased | TrSetCSSDataURLBased | TrTechnicalInfo | TrCustomIssue | TrCSSInsertRuleURLBased | TrMouseClick | TrCreateIFrameDocument | TrAdoptedSSReplaceURLBased | TrAdoptedSSInsertRuleURLBased | TrAdoptedSSDeleteRule | TrAdoptedSSAddOwner | TrAdoptedSSRemoveOwner | TrJSException | TrZustand | TrBatchMetadata | TrPartitionedMessage | TrNetworkRequest | TrInputChange | TrSelectionChange | TrMouseThrashing | TrUnbindNodes | TrResourceTiming | TrTabChange | TrTabData | TrCanvasNode
|
||||||
|
|
||||||
export default function translate(tMsg: TrackerMessage): RawMessage | null {
|
export default function translate(tMsg: TrackerMessage): RawMessage | null {
|
||||||
switch(tMsg[0]) {
|
switch(tMsg[0]) {
|
||||||
|
|
@ -992,6 +998,14 @@ export default function translate(tMsg: TrackerMessage): RawMessage | null {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 119: {
|
||||||
|
return {
|
||||||
|
tp: MType.CanvasNode,
|
||||||
|
nodeId: tMsg[1],
|
||||||
|
timestamp: tMsg[2],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,7 @@ export interface ISession {
|
||||||
metadata: [];
|
metadata: [];
|
||||||
favorite: boolean;
|
favorite: boolean;
|
||||||
filterId?: string;
|
filterId?: string;
|
||||||
|
canvasURL: string[];
|
||||||
domURL: string[];
|
domURL: string[];
|
||||||
devtoolsURL: string[];
|
devtoolsURL: string[];
|
||||||
/**
|
/**
|
||||||
|
|
@ -148,6 +149,7 @@ const emptyValues = {
|
||||||
devtoolsURL: [],
|
devtoolsURL: [],
|
||||||
mobsUrl: [],
|
mobsUrl: [],
|
||||||
notes: [],
|
notes: [],
|
||||||
|
canvasURL: [],
|
||||||
metadata: {},
|
metadata: {},
|
||||||
startedAt: 0,
|
startedAt: 0,
|
||||||
platform: 'web',
|
platform: 'web',
|
||||||
|
|
@ -160,6 +162,7 @@ export default class Session {
|
||||||
siteId: ISession['siteId'];
|
siteId: ISession['siteId'];
|
||||||
projectKey: ISession['projectKey'];
|
projectKey: ISession['projectKey'];
|
||||||
peerId: ISession['peerId'];
|
peerId: ISession['peerId'];
|
||||||
|
canvasURL: ISession['canvasURL'];
|
||||||
live: ISession['live'];
|
live: ISession['live'];
|
||||||
startedAt: ISession['startedAt'];
|
startedAt: ISession['startedAt'];
|
||||||
duration: ISession['duration'];
|
duration: ISession['duration'];
|
||||||
|
|
@ -234,6 +237,7 @@ export default class Session {
|
||||||
mobsUrl = [],
|
mobsUrl = [],
|
||||||
crashes = [],
|
crashes = [],
|
||||||
notes = [],
|
notes = [],
|
||||||
|
canvasURL = [],
|
||||||
...session
|
...session
|
||||||
} = sessionData;
|
} = sessionData;
|
||||||
const duration = Duration.fromMillis(session.duration < 1000 ? 1000 : session.duration);
|
const duration = Duration.fromMillis(session.duration < 1000 ? 1000 : session.duration);
|
||||||
|
|
@ -325,6 +329,7 @@ export default class Session {
|
||||||
domURL,
|
domURL,
|
||||||
devtoolsURL,
|
devtoolsURL,
|
||||||
notes,
|
notes,
|
||||||
|
canvasURL,
|
||||||
notesWithEvents: mixedEventsWithIssues,
|
notesWithEvents: mixedEventsWithIssues,
|
||||||
frustrations: frustrationList,
|
frustrations: frustrationList,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
14
frontend/app/window.d.ts
vendored
Normal file
14
frontend/app/window.d.ts
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
env: {
|
||||||
|
NODE_ENV: string
|
||||||
|
ORIGIN: string
|
||||||
|
ASSETS_HOST: string
|
||||||
|
API_EDP: string
|
||||||
|
VERSION: string
|
||||||
|
TRACKER_VERSION: string
|
||||||
|
TRACKER_HOST: string
|
||||||
|
SOURCEMAP: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -25,6 +25,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^5.2.5",
|
"@ant-design/icons": "^5.2.5",
|
||||||
|
"@babel/plugin-transform-private-methods": "^7.23.3",
|
||||||
"@floating-ui/react-dom-interactions": "^0.10.3",
|
"@floating-ui/react-dom-interactions": "^0.10.3",
|
||||||
"@sentry/browser": "^5.21.1",
|
"@sentry/browser": "^5.21.1",
|
||||||
"@svg-maps/world": "^1.0.1",
|
"@svg-maps/world": "^1.0.1",
|
||||||
|
|
@ -89,7 +90,7 @@
|
||||||
"@babel/plugin-proposal-decorators": "^7.23.2",
|
"@babel/plugin-proposal-decorators": "^7.23.2",
|
||||||
"@babel/plugin-proposal-private-methods": "^7.18.6",
|
"@babel/plugin-proposal-private-methods": "^7.18.6",
|
||||||
"@babel/plugin-syntax-bigint": "^7.8.3",
|
"@babel/plugin-syntax-bigint": "^7.8.3",
|
||||||
"@babel/plugin-transform-class-properties": "^7.22.5",
|
"@babel/plugin-transform-class-properties": "^7.23.3",
|
||||||
"@babel/plugin-transform-private-property-in-object": "^7.22.11",
|
"@babel/plugin-transform-private-property-in-object": "^7.22.11",
|
||||||
"@babel/plugin-transform-runtime": "^7.17.12",
|
"@babel/plugin-transform-runtime": "^7.17.12",
|
||||||
"@babel/preset-env": "^7.23.2",
|
"@babel/preset-env": "^7.23.2",
|
||||||
|
|
|
||||||
|
|
@ -1626,6 +1626,18 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@babel/plugin-transform-class-properties@npm:^7.23.3":
|
||||||
|
version: 7.23.3
|
||||||
|
resolution: "@babel/plugin-transform-class-properties@npm:7.23.3"
|
||||||
|
dependencies:
|
||||||
|
"@babel/helper-create-class-features-plugin": ^7.22.15
|
||||||
|
"@babel/helper-plugin-utils": ^7.22.5
|
||||||
|
peerDependencies:
|
||||||
|
"@babel/core": ^7.0.0-0
|
||||||
|
checksum: bca30d576f539eef216494b56d610f1a64aa9375de4134bc021d9660f1fa735b1d7cc413029f22abc0b7cb737e3a57935c8ae9d8bd1730921ccb1deebce51bfd
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@babel/plugin-transform-class-static-block@npm:^7.22.11":
|
"@babel/plugin-transform-class-static-block@npm:^7.22.11":
|
||||||
version: 7.22.11
|
version: 7.22.11
|
||||||
resolution: "@babel/plugin-transform-class-static-block@npm:7.22.11"
|
resolution: "@babel/plugin-transform-class-static-block@npm:7.22.11"
|
||||||
|
|
@ -2227,6 +2239,18 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@babel/plugin-transform-private-methods@npm:^7.23.3":
|
||||||
|
version: 7.23.3
|
||||||
|
resolution: "@babel/plugin-transform-private-methods@npm:7.23.3"
|
||||||
|
dependencies:
|
||||||
|
"@babel/helper-create-class-features-plugin": ^7.22.15
|
||||||
|
"@babel/helper-plugin-utils": ^7.22.5
|
||||||
|
peerDependencies:
|
||||||
|
"@babel/core": ^7.0.0-0
|
||||||
|
checksum: 745a655edcd111b7f91882b921671ca0613079760d8c9befe336b8a9bc4ce6bb49c0c08941831c950afb1b225b4b2d3eaac8842e732db095b04db38efd8c34f4
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@babel/plugin-transform-private-property-in-object@npm:^7.22.11":
|
"@babel/plugin-transform-private-property-in-object@npm:^7.22.11":
|
||||||
version: 7.22.11
|
version: 7.22.11
|
||||||
resolution: "@babel/plugin-transform-private-property-in-object@npm:7.22.11"
|
resolution: "@babel/plugin-transform-private-property-in-object@npm:7.22.11"
|
||||||
|
|
@ -19630,7 +19654,8 @@ __metadata:
|
||||||
"@babel/plugin-proposal-decorators": ^7.23.2
|
"@babel/plugin-proposal-decorators": ^7.23.2
|
||||||
"@babel/plugin-proposal-private-methods": ^7.18.6
|
"@babel/plugin-proposal-private-methods": ^7.18.6
|
||||||
"@babel/plugin-syntax-bigint": ^7.8.3
|
"@babel/plugin-syntax-bigint": ^7.8.3
|
||||||
"@babel/plugin-transform-class-properties": ^7.22.5
|
"@babel/plugin-transform-class-properties": ^7.23.3
|
||||||
|
"@babel/plugin-transform-private-methods": ^7.23.3
|
||||||
"@babel/plugin-transform-private-property-in-object": ^7.22.11
|
"@babel/plugin-transform-private-property-in-object": ^7.22.11
|
||||||
"@babel/plugin-transform-runtime": ^7.17.12
|
"@babel/plugin-transform-runtime": ^7.17.12
|
||||||
"@babel/preset-env": ^7.23.2
|
"@babel/preset-env": ^7.23.2
|
||||||
|
|
|
||||||
|
|
@ -504,6 +504,11 @@ message 118, 'TabData' do
|
||||||
string 'TabId'
|
string 'TabId'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
message 119, 'CanvasNode' do
|
||||||
|
string 'NodeId'
|
||||||
|
uint 'Timestamp'
|
||||||
|
end
|
||||||
|
|
||||||
## Backend-only
|
## Backend-only
|
||||||
message 125, 'IssueEvent', :replayer => false, :tracker => false do
|
message 125, 'IssueEvent', :replayer => false, :tracker => false do
|
||||||
uint 'MessageID'
|
uint 'MessageID'
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
. "$(dirname -- "$0")/_/husky.sh"
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
if git diff --cached --name-only | grep --quiet '^tracker/tracker/'
|
if git diff --cached --name-only | grep -v '\.gen\.ts$' | grep --quiet '^tracker/tracker/'
|
||||||
then
|
then
|
||||||
echo "tracker"
|
echo "tracker"
|
||||||
pwd
|
pwd
|
||||||
|
|
@ -12,7 +12,7 @@ then
|
||||||
cd ../../
|
cd ../../
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if git diff --cached --name-only | grep --quiet '^tracker/tracker-assist/'
|
if git diff --cached --name-only | grep -v '\.gen\.ts$' | grep --quiet '^tracker/tracker-assist/'
|
||||||
then
|
then
|
||||||
echo "tracker-assist"
|
echo "tracker-assist"
|
||||||
cd tracker/tracker-assist
|
cd tracker/tracker-assist
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@openreplay/tracker-assist",
|
"name": "@openreplay/tracker-assist",
|
||||||
"description": "Tracker plugin for screen assistance through the WebRTC",
|
"description": "Tracker plugin for screen assistance through the WebRTC",
|
||||||
"version": "6.0.3",
|
"version": "6.0.4-57",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"WebRTC",
|
"WebRTC",
|
||||||
"assistance",
|
"assistance",
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
"socket.io-client": "^4.7.2"
|
"socket.io-client": "^4.7.2"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@openreplay/tracker": ">=8.0.0"
|
"@openreplay/tracker": ">=10.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@openreplay/tracker": "file:../tracker",
|
"@openreplay/tracker": "file:../tracker",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||||
|
import {hasTag,} from '@openreplay/tracker/lib/app/guards'
|
||||||
import type { Socket, } from 'socket.io-client'
|
import type { Socket, } from 'socket.io-client'
|
||||||
import { connect, } from 'socket.io-client'
|
import { connect, } from 'socket.io-client'
|
||||||
import Peer, { MediaConnection, } from 'peerjs'
|
import Peer, { MediaConnection, } from 'peerjs'
|
||||||
|
|
@ -14,6 +15,7 @@ import { callConfirmDefault, } from './ConfirmWindow/defaults.js'
|
||||||
import type { Options as ConfirmOptions, } from './ConfirmWindow/defaults.js'
|
import type { Options as ConfirmOptions, } from './ConfirmWindow/defaults.js'
|
||||||
import ScreenRecordingState from './ScreenRecordingState.js'
|
import ScreenRecordingState from './ScreenRecordingState.js'
|
||||||
import { pkgVersion, } from './version.js'
|
import { pkgVersion, } from './version.js'
|
||||||
|
import Canvas from './Canvas.js'
|
||||||
|
|
||||||
// TODO: fully specified strict check with no-any (everywhere)
|
// TODO: fully specified strict check with no-any (everywhere)
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|
@ -21,6 +23,14 @@ const safeCastedPeer = Peer.default || Peer
|
||||||
|
|
||||||
type StartEndCallback = (agentInfo?: Record<string, any>) => ((() => any) | void)
|
type StartEndCallback = (agentInfo?: Record<string, any>) => ((() => any) | void)
|
||||||
|
|
||||||
|
interface AgentInfo {
|
||||||
|
email: string;
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
peerId: string
|
||||||
|
query: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
onAgentConnect: StartEndCallback;
|
onAgentConnect: StartEndCallback;
|
||||||
onCallStart: StartEndCallback;
|
onCallStart: StartEndCallback;
|
||||||
|
|
@ -58,7 +68,7 @@ type OptionalCallback = (()=>Record<string, unknown>) | void
|
||||||
type Agent = {
|
type Agent = {
|
||||||
onDisconnect?: OptionalCallback,
|
onDisconnect?: OptionalCallback,
|
||||||
onControlReleased?: OptionalCallback,
|
onControlReleased?: OptionalCallback,
|
||||||
agentInfo: Record<string, string> | undefined
|
agentInfo: AgentInfo | undefined
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,12 +78,15 @@ export default class Assist {
|
||||||
|
|
||||||
private socket: Socket | null = null
|
private socket: Socket | null = null
|
||||||
private peer: Peer | null = null
|
private peer: Peer | null = null
|
||||||
|
private canvasPeer: Peer | null = null
|
||||||
private assistDemandedRestart = false
|
private assistDemandedRestart = false
|
||||||
private callingState: CallingState = CallingState.False
|
private callingState: CallingState = CallingState.False
|
||||||
private remoteControl: RemoteControl | null = null;
|
private remoteControl: RemoteControl | null = null;
|
||||||
|
|
||||||
private agents: Record<string, Agent> = {}
|
private agents: Record<string, Agent> = {}
|
||||||
private readonly options: Options
|
private readonly options: Options
|
||||||
|
private readonly canvasMap: Map<number, Canvas> = new Map()
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly app: App,
|
private readonly app: App,
|
||||||
options?: Partial<Options>,
|
options?: Partial<Options>,
|
||||||
|
|
@ -151,13 +164,14 @@ export default class Assist {
|
||||||
}
|
}
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
private onStart() {
|
private onStart() {
|
||||||
const app = this.app
|
const app = this.app
|
||||||
const sessionId = app.getSessionID()
|
const sessionId = app.getSessionID()
|
||||||
// Common for all incoming call requests
|
// Common for all incoming call requests
|
||||||
let callUI: CallWindow | null = null
|
let callUI: CallWindow | null = null
|
||||||
let annot: AnnotationCanvas | null = null
|
let annot: AnnotationCanvas | null = null
|
||||||
// TODO: incapsulate
|
// TODO: encapsulate
|
||||||
let callConfirmWindow: ConfirmWindow | null = null
|
let callConfirmWindow: ConfirmWindow | null = null
|
||||||
let callConfirmAnswer: Promise<boolean> | null = null
|
let callConfirmAnswer: Promise<boolean> | null = null
|
||||||
let callEndCallback: ReturnType<StartEndCallback> | null = null
|
let callEndCallback: ReturnType<StartEndCallback> | null = null
|
||||||
|
|
@ -190,7 +204,7 @@ export default class Assist {
|
||||||
app.debug.log('Socket:', ...args)
|
app.debug.log('Socket:', ...args)
|
||||||
})
|
})
|
||||||
|
|
||||||
const onGrand = (id) => {
|
const onGrand = (id: string) => {
|
||||||
if (!callUI) {
|
if (!callUI) {
|
||||||
callUI = new CallWindow(app.debug.error, this.options.callUITemplate)
|
callUI = new CallWindow(app.debug.error, this.options.callUITemplate)
|
||||||
}
|
}
|
||||||
|
|
@ -203,7 +217,7 @@ export default class Assist {
|
||||||
annot.mount()
|
annot.mount()
|
||||||
return callingAgents.get(id)
|
return callingAgents.get(id)
|
||||||
}
|
}
|
||||||
const onRelease = (id, isDenied) => {
|
const onRelease = (id?: string | null, isDenied?: boolean) => {
|
||||||
{
|
{
|
||||||
if (id) {
|
if (id) {
|
||||||
const cb = this.agents[id].onControlReleased
|
const cb = this.agents[id].onControlReleased
|
||||||
|
|
@ -237,7 +251,7 @@ export default class Assist {
|
||||||
const onAcceptRecording = () => {
|
const onAcceptRecording = () => {
|
||||||
socket.emit('recording_accepted')
|
socket.emit('recording_accepted')
|
||||||
}
|
}
|
||||||
const onRejectRecording = (agentData) => {
|
const onRejectRecording = (agentData: AgentInfo) => {
|
||||||
socket.emit('recording_rejected')
|
socket.emit('recording_rejected')
|
||||||
|
|
||||||
this.options.onRecordingDeny?.(agentData || {})
|
this.options.onRecordingDeny?.(agentData || {})
|
||||||
|
|
@ -276,7 +290,7 @@ export default class Assist {
|
||||||
socket.on('startAnnotation', (id, event) => processEvent(id, event, (_, d) => annot?.start(d)))
|
socket.on('startAnnotation', (id, event) => processEvent(id, event, (_, d) => annot?.start(d)))
|
||||||
socket.on('stopAnnotation', (id, event) => processEvent(id, event, annot?.stop))
|
socket.on('stopAnnotation', (id, event) => processEvent(id, event, annot?.stop))
|
||||||
|
|
||||||
socket.on('NEW_AGENT', (id: string, info) => {
|
socket.on('NEW_AGENT', (id: string, info: AgentInfo) => {
|
||||||
this.agents[id] = {
|
this.agents[id] = {
|
||||||
onDisconnect: this.options.onAgentConnect?.(info),
|
onDisconnect: this.options.onAgentConnect?.(info),
|
||||||
agentInfo: info, // TODO ?
|
agentInfo: info, // TODO ?
|
||||||
|
|
@ -386,7 +400,7 @@ export default class Assist {
|
||||||
host: this.getHost(),
|
host: this.getHost(),
|
||||||
path: this.getBasePrefixUrl()+'/assist',
|
path: this.getBasePrefixUrl()+'/assist',
|
||||||
port: location.protocol === 'http:' && this.noSecureMode ? 80 : 443,
|
port: location.protocol === 'http:' && this.noSecureMode ? 80 : 443,
|
||||||
//debug: appOptions.__debug_log ? 2 : 0, // 0 Print nothing //1 Prints only errors. / 2 Prints errors and warnings. / 3 Prints all logs.
|
debug: 2, //appOptions.__debug_log ? 2 : 0, // 0 Print nothing //1 Prints only errors. / 2 Prints errors and warnings. / 3 Prints all logs.
|
||||||
}
|
}
|
||||||
if (this.options.config) {
|
if (this.options.config) {
|
||||||
peerOptions['config'] = this.options.config
|
peerOptions['config'] = this.options.config
|
||||||
|
|
@ -547,6 +561,38 @@ export default class Assist {
|
||||||
app.debug.log(reason)
|
app.debug.log(reason)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
app.nodes.attachNodeCallback((node) => {
|
||||||
|
const id = app.nodes.getID(node)
|
||||||
|
if (id && hasTag(node, 'canvas')) {
|
||||||
|
const canvasPId = `${app.getProjectKey()}-${sessionId}-${id}`
|
||||||
|
if (!this.canvasPeer) this.canvasPeer = new safeCastedPeer(canvasPId, peerOptions) as Peer
|
||||||
|
const canvasHandler = new Canvas(
|
||||||
|
node as unknown as HTMLCanvasElement,
|
||||||
|
id,
|
||||||
|
30,
|
||||||
|
(stream: MediaStream) => {
|
||||||
|
Object.values(this.agents).forEach(agent => {
|
||||||
|
if (agent.agentInfo) {
|
||||||
|
const target = `${agent.agentInfo.peerId}-${agent.agentInfo.id}-canvas`
|
||||||
|
const connection = this.canvasPeer?.connect(target)
|
||||||
|
connection?.on('open', () => {
|
||||||
|
if (agent.agentInfo) {
|
||||||
|
const pCall = this.canvasPeer?.call(target, stream)
|
||||||
|
pCall?.on('error', app.debug.error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
connection?.on('error', app.debug.error)
|
||||||
|
this.canvasPeer?.on('error', app.debug.error)
|
||||||
|
} else {
|
||||||
|
app.debug.error('Assist: cant establish canvas peer to agent, no agent info')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
this.canvasMap.set(id, canvasHandler)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private playNotificationSound() {
|
private playNotificationSound() {
|
||||||
|
|
@ -572,3 +618,16 @@ export default class Assist {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** simple peers impl
|
||||||
|
* const slPeer = new SLPeer({ initiator: true, stream: stream, })
|
||||||
|
* // slPeer.on('signal', (data: any) => {
|
||||||
|
* // this.emit('c_signal', { data, id, })
|
||||||
|
* // })
|
||||||
|
* // this.socket?.on('c_signal', (tab: string, data: any) => {
|
||||||
|
* // console.log(data)
|
||||||
|
* // slPeer.signal(data)
|
||||||
|
* // })
|
||||||
|
* // slPeer.on('error', console.error)
|
||||||
|
* // this.emit('canvas_stream', { canvasId, })
|
||||||
|
* */
|
||||||
61
tracker/tracker-assist/src/Canvas.ts
Normal file
61
tracker/tracker-assist/src/Canvas.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
|
||||||
|
export default class CanvasRecorder {
|
||||||
|
stream: MediaStream | null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly canvas: HTMLCanvasElement,
|
||||||
|
private readonly canvasId: number,
|
||||||
|
private readonly fps: number,
|
||||||
|
private readonly onStream: (stream: MediaStream) => void
|
||||||
|
) {
|
||||||
|
this.canvas.getContext('2d', { alpha: true, })
|
||||||
|
const stream = this.canvas.captureStream(this.fps)
|
||||||
|
this.emitStream(stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
restart() {
|
||||||
|
// this.stop()
|
||||||
|
const stream = this.canvas.captureStream(this.fps)
|
||||||
|
this.stream = stream
|
||||||
|
this.emitStream(stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleLocal(stream: MediaStream) {
|
||||||
|
const possibleVideoEl = document.getElementById('canvas-or-testing')
|
||||||
|
if (possibleVideoEl) {
|
||||||
|
document.body.removeChild(possibleVideoEl)
|
||||||
|
}
|
||||||
|
const video = document.createElement('video')
|
||||||
|
video.width = 520
|
||||||
|
video.height = 400
|
||||||
|
video.id = 'canvas-or-testing'
|
||||||
|
video.setAttribute('autoplay', 'true')
|
||||||
|
video.setAttribute('muted', 'true')
|
||||||
|
video.setAttribute('playsinline', 'true')
|
||||||
|
video.crossOrigin = 'anonymous'
|
||||||
|
document.body.appendChild(video)
|
||||||
|
|
||||||
|
video.srcObject = stream
|
||||||
|
|
||||||
|
void video.play()
|
||||||
|
video.addEventListener('error', (e) => {
|
||||||
|
console.error('Video error:', e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
emitStream(stream?: MediaStream) {
|
||||||
|
if (stream) {
|
||||||
|
return this.onStream(stream)
|
||||||
|
}
|
||||||
|
if (this.stream) {
|
||||||
|
this.onStream(this.stream)
|
||||||
|
} else {
|
||||||
|
console.error('no stream for canvas', this.canvasId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
this.stream?.getTracks().forEach((track) => track.stop())
|
||||||
|
this.stream = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1 +1 @@
|
||||||
export const pkgVersion = '6.0.3'
|
export const pkgVersion = '6.0.4-57'
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -1,14 +1,15 @@
|
||||||
{
|
{
|
||||||
"name": "@openreplay/tracker",
|
"name": "@openreplay/tracker",
|
||||||
"description": "The OpenReplay tracker main package",
|
"description": "The OpenReplay tracker main package",
|
||||||
"version": "10.0.2",
|
"version": "10.0.3-43",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"logging",
|
"logging",
|
||||||
"replay"
|
"replay"
|
||||||
],
|
],
|
||||||
"author": "Alex Tsokurov",
|
"author": "Alex Tsokurov",
|
||||||
"contributors": [
|
"contributors": [
|
||||||
"Aleksandr K <alex@openreplay.com>"
|
"Aleksandr K <alex@openreplay.com>",
|
||||||
|
"Nikita D <nikita@openreplay.com>"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@ export declare const enum Type {
|
||||||
ResourceTiming = 116,
|
ResourceTiming = 116,
|
||||||
TabChange = 117,
|
TabChange = 117,
|
||||||
TabData = 118,
|
TabData = 118,
|
||||||
|
CanvasNode = 119,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -562,6 +563,12 @@ export type TabData = [
|
||||||
/*tabId:*/ string,
|
/*tabId:*/ string,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export type CanvasNode = [
|
||||||
|
/*type:*/ Type.CanvasNode,
|
||||||
|
/*nodeId:*/ string,
|
||||||
|
/*timestamp:*/ number,
|
||||||
|
]
|
||||||
|
|
||||||
type Message = Timestamp | SetPageLocation | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetNodeScroll | SetInputTarget | SetInputValue | SetInputChecked | MouseMove | NetworkRequestDeprecated | ConsoleLog | PageLoadTiming | PageRenderTiming | CustomEvent | UserID | UserAnonymousID | Metadata | CSSInsertRule | CSSDeleteRule | Fetch | Profiler | OTable | StateAction | Redux | Vuex | MobX | NgRx | GraphQL | PerformanceTrack | StringDict | SetNodeAttributeDict | ResourceTimingDeprecated | ConnectionInformation | SetPageVisibility | LoadFontFace | SetNodeFocus | LongTask | SetNodeAttributeURLBased | SetCSSDataURLBased | TechnicalInfo | CustomIssue | CSSInsertRuleURLBased | MouseClick | CreateIFrameDocument | AdoptedSSReplaceURLBased | AdoptedSSInsertRuleURLBased | AdoptedSSDeleteRule | AdoptedSSAddOwner | AdoptedSSRemoveOwner | JSException | Zustand | BatchMetadata | PartitionedMessage | NetworkRequest | InputChange | SelectionChange | MouseThrashing | UnbindNodes | ResourceTiming | TabChange | TabData
|
|
||||||
|
type Message = Timestamp | SetPageLocation | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetNodeScroll | SetInputTarget | SetInputValue | SetInputChecked | MouseMove | NetworkRequestDeprecated | ConsoleLog | PageLoadTiming | PageRenderTiming | CustomEvent | UserID | UserAnonymousID | Metadata | CSSInsertRule | CSSDeleteRule | Fetch | Profiler | OTable | StateAction | Redux | Vuex | MobX | NgRx | GraphQL | PerformanceTrack | StringDict | SetNodeAttributeDict | ResourceTimingDeprecated | ConnectionInformation | SetPageVisibility | LoadFontFace | SetNodeFocus | LongTask | SetNodeAttributeURLBased | SetCSSDataURLBased | TechnicalInfo | CustomIssue | CSSInsertRuleURLBased | MouseClick | CreateIFrameDocument | AdoptedSSReplaceURLBased | AdoptedSSInsertRuleURLBased | AdoptedSSDeleteRule | AdoptedSSAddOwner | AdoptedSSRemoveOwner | JSException | Zustand | BatchMetadata | PartitionedMessage | NetworkRequest | InputChange | SelectionChange | MouseThrashing | UnbindNodes | ResourceTiming | TabChange | TabData | CanvasNode
|
||||||
export default Message
|
export default Message
|
||||||
|
|
|
||||||
130
tracker/tracker/src/main/app/canvas.ts
Normal file
130
tracker/tracker/src/main/app/canvas.ts
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
import App from '../app/index.js'
|
||||||
|
import { hasTag } from './guards.js'
|
||||||
|
import Message, { CanvasNode } from './messages.gen.js'
|
||||||
|
|
||||||
|
interface CanvasSnapshot {
|
||||||
|
images: { data: string; id: number }[]
|
||||||
|
createdAt: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Options {
|
||||||
|
fps: number
|
||||||
|
quality: 'low' | 'medium' | 'high'
|
||||||
|
}
|
||||||
|
|
||||||
|
class CanvasRecorder {
|
||||||
|
private snapshots: Record<number, CanvasSnapshot> = {}
|
||||||
|
private readonly intervals: NodeJS.Timeout[] = []
|
||||||
|
private readonly interval: number
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly app: App,
|
||||||
|
private readonly options: Options,
|
||||||
|
) {
|
||||||
|
this.interval = 1000 / options.fps
|
||||||
|
}
|
||||||
|
|
||||||
|
startTracking() {
|
||||||
|
this.app.nodes.attachNodeCallback((node: Node): void => {
|
||||||
|
const id = this.app.nodes.getID(node)
|
||||||
|
if (!id || !hasTag(node, 'canvas') || this.snapshots[id]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const ts = this.app.timestamp()
|
||||||
|
this.snapshots[id] = {
|
||||||
|
images: [],
|
||||||
|
createdAt: ts,
|
||||||
|
}
|
||||||
|
const canvasMsg = CanvasNode(id.toString(), ts)
|
||||||
|
this.app.send(canvasMsg as Message)
|
||||||
|
const int = setInterval(() => {
|
||||||
|
const cid = this.app.nodes.getID(node)
|
||||||
|
const canvas = cid ? this.app.nodes.getNode(cid) : undefined
|
||||||
|
if (!canvas || !hasTag(canvas, 'canvas') || canvas !== node) {
|
||||||
|
console.log('Canvas element not in sync')
|
||||||
|
clearInterval(int)
|
||||||
|
} else {
|
||||||
|
const snapshot = captureSnapshot(canvas, this.options.quality)
|
||||||
|
this.snapshots[id].images.push({ id: this.app.timestamp(), data: snapshot })
|
||||||
|
if (this.snapshots[id].images.length > 9) {
|
||||||
|
this.sendSnaps(this.snapshots[id].images, id, this.snapshots[id].createdAt)
|
||||||
|
this.snapshots[id].images = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, this.interval)
|
||||||
|
this.intervals.push(int)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
sendSnaps(images: { data: string; id: number }[], canvasId: number, createdAt: number) {
|
||||||
|
if (Object.keys(this.snapshots).length === 0) {
|
||||||
|
console.log(this.snapshots)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const formData = new FormData()
|
||||||
|
images.forEach((snapshot) => {
|
||||||
|
const blob = dataUrlToBlob(snapshot.data)[0]
|
||||||
|
formData.append('snapshot', blob, `${createdAt}_${canvasId}_${snapshot.id}.jpeg`)
|
||||||
|
// saveImageData(snapshot.data, `${createdAt}_${canvasId}_${snapshot.id}.jpeg`)
|
||||||
|
})
|
||||||
|
|
||||||
|
fetch(this.app.options.ingestPoint + '/v1/web/images', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.app.session.getSessionToken() ?? ''}`,
|
||||||
|
},
|
||||||
|
body: formData,
|
||||||
|
})
|
||||||
|
.then((r) => {
|
||||||
|
console.log('done', r)
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error('error saving canvas', e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
console.log('cleaning up')
|
||||||
|
this.intervals.forEach((int) => clearInterval(int))
|
||||||
|
this.snapshots = {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const qualityInt = {
|
||||||
|
low: 0.33,
|
||||||
|
medium: 0.55,
|
||||||
|
high: 0.8,
|
||||||
|
}
|
||||||
|
|
||||||
|
function captureSnapshot(canvas: HTMLCanvasElement, quality: 'low' | 'medium' | 'high' = 'medium') {
|
||||||
|
const imageFormat = 'image/jpeg' // or /png'
|
||||||
|
return canvas.toDataURL(imageFormat, qualityInt[quality])
|
||||||
|
}
|
||||||
|
|
||||||
|
function dataUrlToBlob(dataUrl: string): [Blob, Uint8Array] {
|
||||||
|
const [header, base64] = dataUrl.split(',')
|
||||||
|
// @ts-ignore
|
||||||
|
const mime = header.match(/:(.*?);/)[1]
|
||||||
|
const blobStr = atob(base64)
|
||||||
|
let n = blobStr.length
|
||||||
|
const u8arr = new Uint8Array(n)
|
||||||
|
|
||||||
|
while (n--) {
|
||||||
|
u8arr[n] = blobStr.charCodeAt(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
return [new Blob([u8arr], { type: mime }), u8arr]
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveImageData(imageDataUrl: string, name: string) {
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = imageDataUrl
|
||||||
|
link.download = name
|
||||||
|
link.style.display = 'none'
|
||||||
|
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CanvasRecorder
|
||||||
|
|
@ -38,6 +38,7 @@ type TagTypeMap = {
|
||||||
iframe: HTMLIFrameElement
|
iframe: HTMLIFrameElement
|
||||||
style: HTMLStyleElement | SVGStyleElement
|
style: HTMLStyleElement | SVGStyleElement
|
||||||
link: HTMLLinkElement
|
link: HTMLLinkElement
|
||||||
|
canvas: HTMLCanvasElement
|
||||||
}
|
}
|
||||||
export function hasTag<T extends keyof TagTypeMap>(
|
export function hasTag<T extends keyof TagTypeMap>(
|
||||||
el: Node,
|
el: Node,
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import type { Options as SanitizerOptions } from './sanitizer.js'
|
||||||
import type { Options as LoggerOptions } from './logger.js'
|
import type { Options as LoggerOptions } from './logger.js'
|
||||||
import type { Options as SessOptions } from './session.js'
|
import type { Options as SessOptions } from './session.js'
|
||||||
import type { Options as NetworkOptions } from '../modules/network.js'
|
import type { Options as NetworkOptions } from '../modules/network.js'
|
||||||
|
import CanvasRecorder from './canvas.js'
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
Options as WebworkerOptions,
|
Options as WebworkerOptions,
|
||||||
|
|
@ -142,6 +143,12 @@ export default class App {
|
||||||
private readonly bc: BroadcastChannel | null = null
|
private readonly bc: BroadcastChannel | null = null
|
||||||
private readonly contextId
|
private readonly contextId
|
||||||
public attributeSender: AttributeSender
|
public attributeSender: AttributeSender
|
||||||
|
private canvasRecorder: CanvasRecorder | null = null
|
||||||
|
private canvasOptions = {
|
||||||
|
canvasEnabled: false,
|
||||||
|
canvasQuality: 'medium',
|
||||||
|
canvasFPS: 1,
|
||||||
|
}
|
||||||
|
|
||||||
constructor(projectKey: string, sessionToken: string | undefined, options: Partial<Options>) {
|
constructor(projectKey: string, sessionToken: string | undefined, options: Partial<Options>) {
|
||||||
// if (options.onStart !== undefined) {
|
// if (options.onStart !== undefined) {
|
||||||
|
|
@ -631,6 +638,9 @@ export default class App {
|
||||||
userDevice,
|
userDevice,
|
||||||
userOS,
|
userOS,
|
||||||
userState,
|
userState,
|
||||||
|
canvasEnabled,
|
||||||
|
canvasQuality,
|
||||||
|
canvasFPS,
|
||||||
} = r
|
} = r
|
||||||
if (
|
if (
|
||||||
typeof token !== 'string' ||
|
typeof token !== 'string' ||
|
||||||
|
|
@ -675,13 +685,19 @@ export default class App {
|
||||||
})
|
})
|
||||||
|
|
||||||
this.compressionThreshold = compressionThreshold
|
this.compressionThreshold = compressionThreshold
|
||||||
|
|
||||||
const onStartInfo = { sessionToken: token, userUUID, sessionID }
|
const onStartInfo = { sessionToken: token, userUUID, sessionID }
|
||||||
|
|
||||||
// TODO: start as early as possible (before receiving the token)
|
// TODO: start as early as possible (before receiving the token)
|
||||||
this.startCallbacks.forEach((cb) => cb(onStartInfo)) // MBTODO: callbacks after DOM "mounted" (observed)
|
this.startCallbacks.forEach((cb) => cb(onStartInfo)) // MBTODO: callbacks after DOM "mounted" (observed)
|
||||||
this.observer.observe()
|
this.observer.observe()
|
||||||
this.ticker.start()
|
this.ticker.start()
|
||||||
|
|
||||||
|
if (canvasEnabled) {
|
||||||
|
this.canvasRecorder =
|
||||||
|
this.canvasRecorder ??
|
||||||
|
new CanvasRecorder(this, { fps: canvasFPS, quality: canvasQuality })
|
||||||
|
this.canvasRecorder.startTracking()
|
||||||
|
}
|
||||||
this.activityState = ActivityState.Active
|
this.activityState = ActivityState.Active
|
||||||
|
|
||||||
this.notify.log('OpenReplay tracking started.')
|
this.notify.log('OpenReplay tracking started.')
|
||||||
|
|
@ -752,6 +768,7 @@ export default class App {
|
||||||
if (this.worker && stopWorker) {
|
if (this.worker && stopWorker) {
|
||||||
this.worker.postMessage('stop')
|
this.worker.postMessage('stop')
|
||||||
}
|
}
|
||||||
|
this.canvasRecorder?.clear()
|
||||||
} finally {
|
} finally {
|
||||||
this.activityState = ActivityState.NotActive
|
this.activityState = ActivityState.NotActive
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -912,3 +912,14 @@ export function TabData(
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function CanvasNode(
|
||||||
|
nodeId: string,
|
||||||
|
timestamp: number,
|
||||||
|
): Messages.CanvasNode {
|
||||||
|
return [
|
||||||
|
Messages.Type.CanvasNode,
|
||||||
|
nodeId,
|
||||||
|
timestamp,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
71
tracker/tracker/src/tests/guards.test.ts
Normal file
71
tracker/tracker/src/tests/guards.test.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { describe, beforeEach, expect, test } from '@jest/globals'
|
||||||
|
import {
|
||||||
|
isNode,
|
||||||
|
isSVGElement,
|
||||||
|
isElementNode,
|
||||||
|
isCommentNode,
|
||||||
|
isTextNode,
|
||||||
|
isDocument,
|
||||||
|
isRootNode,
|
||||||
|
hasTag,
|
||||||
|
} from '../main/app/guards'
|
||||||
|
|
||||||
|
describe('DOM utility functions', () => {
|
||||||
|
let elementNode: Element
|
||||||
|
let commentNode: Comment
|
||||||
|
let textNode: Text
|
||||||
|
let documentNode: Document
|
||||||
|
let fragmentNode: DocumentFragment
|
||||||
|
let svgElement: SVGElement
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
elementNode = document.createElement('div')
|
||||||
|
commentNode = document.createComment('This is a comment')
|
||||||
|
textNode = document.createTextNode('This is text')
|
||||||
|
documentNode = document
|
||||||
|
fragmentNode = document.createDocumentFragment()
|
||||||
|
svgElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('isNode', () => {
|
||||||
|
expect(isNode(elementNode)).toBeTruthy()
|
||||||
|
expect(isNode(null)).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('isSVGElement', () => {
|
||||||
|
expect(isSVGElement(svgElement)).toBeTruthy()
|
||||||
|
expect(isSVGElement(elementNode)).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('isElementNode', () => {
|
||||||
|
expect(isElementNode(elementNode)).toBeTruthy()
|
||||||
|
expect(isElementNode(textNode)).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('isCommentNode', () => {
|
||||||
|
expect(isCommentNode(commentNode)).toBeTruthy()
|
||||||
|
expect(isCommentNode(elementNode)).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('isTextNode', () => {
|
||||||
|
expect(isTextNode(textNode)).toBeTruthy()
|
||||||
|
expect(isTextNode(elementNode)).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('isDocument', () => {
|
||||||
|
expect(isDocument(documentNode)).toBeTruthy()
|
||||||
|
expect(isDocument(elementNode)).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('isRootNode', () => {
|
||||||
|
expect(isRootNode(documentNode)).toBeTruthy()
|
||||||
|
expect(isRootNode(fragmentNode)).toBeTruthy()
|
||||||
|
expect(isRootNode(elementNode)).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('hasTag', () => {
|
||||||
|
const imgElement = document.createElement('img')
|
||||||
|
expect(hasTag(imgElement, 'img')).toBeTruthy()
|
||||||
|
expect(hasTag(elementNode, 'img')).toBeFalsy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import Nodes from './nodes'
|
import Nodes from '../main/app/nodes'
|
||||||
import { describe, beforeEach, expect, it, jest } from '@jest/globals'
|
import { describe, beforeEach, expect, test, jest } from '@jest/globals'
|
||||||
|
|
||||||
describe('Nodes', () => {
|
describe('Nodes', () => {
|
||||||
let nodes: Nodes
|
let nodes: Nodes
|
||||||
|
|
@ -11,13 +11,13 @@ describe('Nodes', () => {
|
||||||
mockCallback.mockClear()
|
mockCallback.mockClear()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('attachNodeCallback', () => {
|
test('attachNodeCallback', () => {
|
||||||
nodes.attachNodeCallback(mockCallback)
|
nodes.attachNodeCallback(mockCallback)
|
||||||
nodes.callNodeCallbacks(document.createElement('div'), true)
|
nodes.callNodeCallbacks(document.createElement('div'), true)
|
||||||
expect(mockCallback).toHaveBeenCalled()
|
expect(mockCallback).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('attachNodeListener is listening to events', () => {
|
test('attachNodeListener is listening to events', () => {
|
||||||
const node = document.createElement('div')
|
const node = document.createElement('div')
|
||||||
const mockListener = jest.fn()
|
const mockListener = jest.fn()
|
||||||
document.body.appendChild(node)
|
document.body.appendChild(node)
|
||||||
|
|
@ -26,7 +26,7 @@ describe('Nodes', () => {
|
||||||
node.dispatchEvent(new Event('click'))
|
node.dispatchEvent(new Event('click'))
|
||||||
expect(mockListener).toHaveBeenCalled()
|
expect(mockListener).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
it('attachNodeListener is calling native method', () => {
|
test('attachNodeListener is calling native method', () => {
|
||||||
const node = document.createElement('div')
|
const node = document.createElement('div')
|
||||||
const mockListener = jest.fn()
|
const mockListener = jest.fn()
|
||||||
const addEventListenerSpy = jest.spyOn(node, 'addEventListener')
|
const addEventListenerSpy = jest.spyOn(node, 'addEventListener')
|
||||||
|
|
@ -36,55 +36,55 @@ describe('Nodes', () => {
|
||||||
expect(addEventListenerSpy).toHaveBeenCalledWith('click', mockListener, true)
|
expect(addEventListenerSpy).toHaveBeenCalledWith('click', mockListener, true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('registerNode', () => {
|
test('registerNode', () => {
|
||||||
const node = document.createElement('div')
|
const node = document.createElement('div')
|
||||||
const [id, isNew] = nodes.registerNode(node)
|
const [id, isNew] = nodes.registerNode(node)
|
||||||
expect(id).toBeDefined()
|
expect(id).toBeDefined()
|
||||||
expect(isNew).toBe(true)
|
expect(isNew).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('unregisterNode', () => {
|
test('unregisterNode', () => {
|
||||||
const node = document.createElement('div')
|
const node = document.createElement('div')
|
||||||
const [id] = nodes.registerNode(node)
|
const [id] = nodes.registerNode(node)
|
||||||
const unregisteredId = nodes.unregisterNode(node)
|
const unregisteredId = nodes.unregisterNode(node)
|
||||||
expect(unregisteredId).toBe(id)
|
expect(unregisteredId).toBe(id)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('cleanTree', () => {
|
test('cleanTree', () => {
|
||||||
const node = document.createElement('div')
|
const node = document.createElement('div')
|
||||||
nodes.registerNode(node)
|
nodes.registerNode(node)
|
||||||
nodes.cleanTree()
|
nodes.cleanTree()
|
||||||
expect(nodes.getNodeCount()).toBe(0)
|
expect(nodes.getNodeCount()).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('callNodeCallbacks', () => {
|
test('callNodeCallbacks', () => {
|
||||||
nodes.attachNodeCallback(mockCallback)
|
nodes.attachNodeCallback(mockCallback)
|
||||||
const node = document.createElement('div')
|
const node = document.createElement('div')
|
||||||
nodes.callNodeCallbacks(node, true)
|
nodes.callNodeCallbacks(node, true)
|
||||||
expect(mockCallback).toHaveBeenCalledWith(node, true)
|
expect(mockCallback).toHaveBeenCalledWith(node, true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('getID', () => {
|
test('getID', () => {
|
||||||
const node = document.createElement('div')
|
const node = document.createElement('div')
|
||||||
const [id] = nodes.registerNode(node)
|
const [id] = nodes.registerNode(node)
|
||||||
const fetchedId = nodes.getID(node)
|
const fetchedId = nodes.getID(node)
|
||||||
expect(fetchedId).toBe(id)
|
expect(fetchedId).toBe(id)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('getNode', () => {
|
test('getNode', () => {
|
||||||
const node = document.createElement('div')
|
const node = document.createElement('div')
|
||||||
const [id] = nodes.registerNode(node)
|
const [id] = nodes.registerNode(node)
|
||||||
const fetchedNode = nodes.getNode(id)
|
const fetchedNode = nodes.getNode(id)
|
||||||
expect(fetchedNode).toBe(node)
|
expect(fetchedNode).toBe(node)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('getNodeCount', () => {
|
test('getNodeCount', () => {
|
||||||
expect(nodes.getNodeCount()).toBe(0)
|
expect(nodes.getNodeCount()).toBe(0)
|
||||||
nodes.registerNode(document.createElement('div'))
|
nodes.registerNode(document.createElement('div'))
|
||||||
expect(nodes.getNodeCount()).toBe(1)
|
expect(nodes.getNodeCount()).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('clear', () => {
|
test('clear', () => {
|
||||||
nodes.registerNode(document.createElement('div'))
|
nodes.registerNode(document.createElement('div'))
|
||||||
nodes.clear()
|
nodes.clear()
|
||||||
expect(nodes.getNodeCount()).toBe(0)
|
expect(nodes.getNodeCount()).toBe(0)
|
||||||
|
|
@ -286,6 +286,10 @@ export default class MessageEncoder extends PrimitiveEncoder {
|
||||||
return this.string(msg[1])
|
return this.string(msg[1])
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case Messages.Type.CanvasNode:
|
||||||
|
return this.string(msg[1]) && this.uint(msg[2])
|
||||||
|
break
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue