diff --git a/frontend/app/player/web/assist/Call.ts b/frontend/app/player/web/assist/Call.ts index 659e8b796..87e9d3715 100644 --- a/frontend/app/player/web/assist/Call.ts +++ b/frontend/app/player/web/assist/Call.ts @@ -1,6 +1,3 @@ -import type Peer from 'peerjs'; -import type { MediaConnection } from 'peerjs'; - import type { LocalStream } from './LocalStream'; import type { Socket } from './types'; import type { Store } from '../../common/types'; @@ -25,9 +22,8 @@ export default class Call { calling: CallingState.NoCall, }; - private _peer: Peer | null = null; - private connectionAttempts: number = 0; - private callConnection: MediaConnection[] = []; + private connections: Record = {}; + private connectAttempts = 0; private videoStreams: Record = {}; constructor( @@ -37,6 +33,7 @@ export default class Call { private peerID: string, private getAssistVersion: () => number ) { + // Обработка событий сокета socket.on('call_end', () => { this.onRemoteCallEnd() }); @@ -56,14 +53,13 @@ export default class Call { }); socket.on('messages_gz', () => { if (reconnecting) { - // 'messages' come frequently, so it is better to have Reconnecting + // При восстановлении соединения инициируем повторное создание соединения this._callSessionPeer(); reconnecting = false; } }) socket.on('messages', () => { if (reconnecting) { - // 'messages' come frequently, so it is better to have Reconnecting this._callSessionPeer(); reconnecting = false; } @@ -71,94 +67,176 @@ export default class Call { socket.on('disconnect', () => { this.store.update({ calling: CallingState.NoCall }); }); + + socket.on('webrtc_offer', (data: { from: string, offer: RTCSessionDescriptionInit }) => { + this.handleOffer(data); + }); + socket.on('webrtc_answer', (data: { from: string, answer: RTCSessionDescriptionInit }) => { + this.handleAnswer(data); + }); + socket.on('webrtc_ice_candidate', (data: { from: string, candidate: RTCIceCandidateInit }) => { + this.handleIceCandidate(data); + }); + this.assistVersion = this.getAssistVersion(); } - private getPeer(): Promise { - if (this._peer && !this._peer.disconnected) { - return Promise.resolve(this._peer); + private async createPeerConnection(remotePeerId: string): Promise { + const pc = new RTCPeerConnection({ + iceServers: [{ urls: "stun:stun.l.google.com:19302" }], + }); + console.log("PC1", pc) + + // Если есть локальный поток, добавляем его треки в соединение. + if (this.callArgs && this.callArgs.localStream && this.callArgs.localStream.stream) { + this.callArgs.localStream.stream.getTracks().forEach((track) => { + pc.addTrack(track, this.callArgs!.localStream.stream); + }); } - // @ts-ignore - const urlObject = new URL(window.env.API_EDP || window.location.origin); - - // @ts-ignore TODO: set module in ts settings - return import('peerjs').then(({ default: Peer }) => { - if (this.cleaned) { - return Promise.reject('Already cleaned'); + pc.onicecandidate = (event) => { + console.log("ICE GENERATED"); + if (event.candidate) { + this.socket.emit('webrtc_ice_candidate', { to: remotePeerId, candidate: event.candidate }); + } else { + console.log("Сбор ICE-кандидатов завершён."); } - 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 peer = (this._peer = new Peer(peerOpts)); - peer.on('call', (call) => { - console.log('getting call from', call.peer); - call.answer(this.callArgs?.localStream.stream); - this.callConnection.push(call); + }; - this.callArgs?.localStream.onVideoTrack((vTrack) => { - const sender = call.peerConnection.getSenders().find((s) => s.track?.kind === 'video'); - if (!sender) { - console.warn('No video sender found'); - return; - } - sender.replaceTrack(vTrack); - }); - - call.on('stream', (stream) => { - this.videoStreams[call.peer] = stream.getVideoTracks()[0]; - this.callArgs && this.callArgs.onStream(stream); - }); - - call.on('close', this.onRemoteCallEnd); - call.on('error', (e) => { - console.error('PeerJS error (on call):', e); - this.initiateCallEnd(); - this.callArgs && this.callArgs.onError && this.callArgs.onError(); - }); - }); - peer.on('error', (e) => { - if (e.type === 'disconnected') { - return peer.reconnect(); - } else if (e.type !== 'peer-unavailable') { - console.error(`PeerJS error (on peer). Type ${e.type}`, e); + pc.ontrack = (event) => { + const stream = event.streams[0]; + if (stream) { + this.videoStreams[remotePeerId] = stream.getVideoTracks()[0]; + if (this.store.get().calling !== CallingState.OnCall) { + this.store.update({ calling: CallingState.OnCall }); } - }); + if (this.callArgs) { + this.callArgs.onStream(stream); + } + } + }; - return new Promise((resolve) => { - peer.on('open', () => resolve(peer)); + // Следим за состоянием соединения + pc.onconnectionstatechange = () => { + if (pc.connectionState === "disconnected" || pc.connectionState === "failed") { + this.onRemoteCallEnd(); + } + }; + + // Обработка замены трека при изменении локального видео + if (this.callArgs && this.callArgs.localStream) { + this.callArgs.localStream.onVideoTrack((vTrack: MediaStreamTrack) => { + const sender = pc.getSenders().find((s) => s.track?.kind === 'video'); + if (!sender) { + console.warn('No video sender found'); + return; + } + sender.replaceTrack(vTrack); }); - }); + } + + return pc; + } + + private async _peerConnection(remotePeerId: string) { + try { + // Создаём RTCPeerConnection + const pc = await this.createPeerConnection(remotePeerId); + this.connections[remotePeerId] = pc; + + // Создаём SDP offer + const offer = await pc.createOffer(); + await pc.setLocalDescription(offer); + + // Отправляем offer + console.log('sending webrtc_offer to', remotePeerId); + this.socket.emit('webrtc_call_offer', { to: remotePeerId, offer: offer }); + this.connectAttempts = 0; + } catch (e: any) { + console.error(e); + // Пробуем переподключиться + const tryReconnect = async (error: any) => { + console.log(error.type, this.connectAttempts); + if (error.type === 'peer-unavailable' && this.connectAttempts < 5) { + this.connectAttempts++; + console.log('reconnecting', this.connectAttempts); + await new Promise((resolve) => setTimeout(resolve, 250)); + await this._peerConnection(remotePeerId); + } else { + console.log('error', this.connectAttempts); + this.callArgs?.onError?.('Could not establish a connection with the peer after 5 attempts'); + } + }; + await tryReconnect(e); + } + } + + private async handleOffer(data: { from: string, offer: RTCSessionDescriptionInit }) { + const remotePeerId = data.from; + try { + const pc = await this.createPeerConnection(remotePeerId); + this.connections[remotePeerId] = pc; + + await pc.setRemoteDescription(new RTCSessionDescription(data.offer)); + + // Генерируем answer и устанавливаем локальное описание + const answer = await pc.createAnswer(); + await pc.setLocalDescription(answer); + + // Отправляем answer + this.socket.emit('webrtc_call_answer', { to: remotePeerId, answer: answer }); + } catch (e) { + console.error("Error handling offer:", e); + this.callArgs?.onError?.(e); + } + } + + private async handleAnswer(data: { from: string, answer: RTCSessionDescriptionInit }) { + const remotePeerId = data.from; + const pc = this.connections[remotePeerId]; + if (!pc) { + console.error("No connection found for remote peer", remotePeerId); + return; + } + try { + await pc.setRemoteDescription(new RTCSessionDescription(data.answer)); + } catch (e) { + console.error("Error setting remote description from answer", e); + this.callArgs?.onError?.(e); + } + } + + private async handleIceCandidate(data: { from: string, candidate: RTCIceCandidateInit }) { + const remotePeerId = data.from; + const pc = this.connections[remotePeerId]; + if (!pc) return; + if (data.candidate && (data.candidate.sdpMid || data.candidate.sdpMLineIndex !== null)) { + try { + await pc.addIceCandidate(new RTCIceCandidate(data.candidate)); + } catch (e) { + console.error("Error adding ICE candidate", e); + } + } else { + console.warn("Пропущен некорректный ICE-кандидат:", data.candidate); + } } private handleCallEnd() { - if (this.store.get().calling !== CallingState.NoCall) this.callArgs && this.callArgs.onCallEnd(); + if (this.store.get().calling !== CallingState.NoCall) { + this.callArgs && this.callArgs.onCallEnd(); + } this.store.update({ calling: CallingState.NoCall }); - this.callConnection[0] && this.callConnection[0].close(); + // Закрываем все созданные RTCPeerConnection + Object.values(this.connections).forEach((pc) => pc.close()); + this.connections = {}; this.callArgs = null; - // TODO: We have it separated, right? (check) - //this.toggleAnnotation(false) } + // Обработчик события завершения вызова по сигналу private onRemoteCallEnd = () => { if ([CallingState.Requesting, CallingState.Connecting].includes(this.store.get().calling)) { this.callArgs && this.callArgs.onReject(); - this.callConnection[0] && this.callConnection[0].close(); + Object.values(this.connections).forEach((pc) => pc.close()); this.store.update({ calling: CallingState.NoCall }); this.callArgs = null; } else { @@ -166,16 +244,11 @@ export default class Call { } }; + // Завершает вызов и отправляет сигнал call_end initiateCallEnd = async () => { const userName = userStore.account.name; this.emitData('call_end', userName); this.handleCallEnd(); - // TODO: We have it separated, right? (check) - // const remoteControl = this.store.get().remoteControl - // if (remoteControl === RemoteControlStatus.Enabled) { - // this.socket.emit("release_control") - // this.toggleRemoteControl(false) - // } }; private emitData = (event: string, data?: any) => { @@ -210,6 +283,9 @@ export default class Call { }; } + /** + * Инициирует вызов + */ call(thirdPartyPeers?: string[]): { end: () => void } { if (thirdPartyPeers && thirdPartyPeers.length > 0) { this.addPeerCall(thirdPartyPeers); @@ -221,106 +297,46 @@ export default class Call { }; } + // Уведомление пиров об изменении состояния локального видео toggleVideoLocalStream(enabled: boolean) { - this.getPeer().then((peer) => { - this.emitData('videofeed', { streamId: peer.id, enabled }); - }); + // Передаём сигнал через socket + this.socket.emit('videofeed', { streamId: this.peerID, enabled }); } - /** Connecting to the other agents that are already - * in the call with the user + /** + * Соединение с другими агентами */ addPeerCall(thirdPartyPeers: string[]) { - thirdPartyPeers.forEach((peer) => this._peerConnection(peer)); + thirdPartyPeers.forEach((peerId) => this._peerConnection(peerId)); } - /** Connecting to the app user */ + /** + * Соединение с основным пользователем приложения. + */ private _callSessionPeer() { if (![CallingState.NoCall, CallingState.Reconnecting].includes(this.store.get().calling)) { return; } this.store.update({ calling: CallingState.Connecting }); const tab = this.store.get().currentTab; - if (!this.store.get().currentTab) { + if (!tab) { console.warn('No tab data to connect to peer'); } + // Формируем идентификатор пира в зависимости от версии ассиста const peerId = this.getAssistVersion() === 1 ? this.peerID - : `${this.peerID}-${tab || Object.keys(this.store.get().tabs)[0]}`; + : `${this.peerID}-${tab || Array.from(this.store.get().tabs)[0]}`; const userName = userStore.account.name; this.emitData('_agent_name', userName); void this._peerConnection(peerId); } - connectAttempts = 0; - private async _peerConnection(remotePeerId: string) { - try { - const peer = await this.getPeer(); - // let canCall = false - - const tryReconnect = async (e: any) => { - peer.off('error', tryReconnect) - console.log(e.type, this.connectAttempts); - if (e.type === 'peer-unavailable' && this.connectAttempts < 5) { - this.connectAttempts++; - console.log('reconnecting', this.connectAttempts); - await new Promise((resolve) => setTimeout(resolve, 250)); - await this._peerConnection(remotePeerId); - } else { - console.log('error', this.connectAttempts); - this.callArgs?.onError?.('Could not establish a connection with the peer after 5 attempts'); - } - } - const call = peer.call(remotePeerId, this.callArgs!.localStream.stream); - peer.on('error', tryReconnect); - - peer.on('connection', () => { - this.callConnection.push(call); - this.connectAttempts = 0; - - this.callArgs?.localStream.onVideoTrack((vTrack) => { - const sender = call.peerConnection.getSenders().find((s) => s.track?.kind === 'video'); - if (!sender) { - console.warn('No video sender found'); - return; - } - sender.replaceTrack(vTrack); - }); - }) - - call.on('stream', (stream) => { - this.store.get().calling !== CallingState.OnCall && - this.store.update({ calling: CallingState.OnCall }); - - this.videoStreams[call.peer] = stream.getVideoTracks()[0]; - - this.callArgs && this.callArgs.onStream(stream); - }); - - call.on('close', this.onRemoteCallEnd); - call.on('error', (e) => { - console.error('PeerJS error (on call):', e); - this.initiateCallEnd(); - this.callArgs && this.callArgs.onError && this.callArgs.onError(); - }); - } catch (e) { - console.error(e); - } - } - - private cleaned: boolean = false; - + // Метод для очистки ресурсов clean() { - this.cleaned = true; // sometimes cleaned before modules loaded void this.initiateCallEnd(); - if (this._peer) { - console.log('destroying peer...'); - const peer = this._peer; // otherwise it calls reconnection on data chan close - this._peer = null; - peer.disconnect(); - peer.destroy(); - } + Object.values(this.connections).forEach((pc) => pc.close()); + this.connections = {}; } } diff --git a/frontend/app/player/web/assist/CanvasReceiver.ts b/frontend/app/player/web/assist/CanvasReceiver.ts index 17ce3605a..49d13c33f 100644 --- a/frontend/app/player/web/assist/CanvasReceiver.ts +++ b/frontend/app/player/web/assist/CanvasReceiver.ts @@ -1,4 +1,3 @@ -import Peer from 'peerjs'; import { VElement } from 'Player/web/managers/DOM/VirtualDOM'; import MessageManager from 'Player/web/MessageManager'; @@ -18,49 +17,45 @@ function draw( export default class CanvasReceiver { private streams: Map = new Map(); - private peer: Peer | null = null; + // Храним RTCPeerConnection для каждого удалённого пира + private connections: Map = new Map(); + private id: string; + //sendSignal – для отправки сигналов (offer/answer/ICE) constructor( private readonly peerIdPrefix: string, private readonly config: RTCIceServer[] | null, private readonly getNode: MessageManager['getNode'], - private readonly agentInfo: Record + private readonly agentInfo: Record, + private readonly sendSignal: (data: any) => void ) { - // @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), + // Формируем идентификатор как в PeerJS + this.id = `${this.peerIdPrefix}-${this.agentInfo.id}-canvas`; + } + + async handleOffer(from: string, offer: RTCSessionDescriptionInit): Promise { + const pc = new RTCPeerConnection({ + iceServers: this.config ? this.config : [{ urls: "stun:stun.l.google.com:19302" }], + }); + + // Сохраняем соединение + this.connections.set(from, pc); + + pc.onicecandidate = (event) => { + if (event.candidate) { + this.sendSignal({ to: from, type: 'canvas_ice_candidate', candidate: event.candidate }); + } }; - 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) => { + + pc.ontrack = (event) => { + const stream = event.streams[0]; + if (stream) { + // Определяем canvasId из удалённого peer id + const canvasId = from.split('-')[2]; 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 - ); + const videoEl = spawnVideo(stream.clone() as MediaStream, node as VElement); if (node) { draw( videoEl, @@ -69,19 +64,34 @@ export default class CanvasReceiver { ); } }, 250); - }); - call.on('error', (err) => console.error('canvas call error', err)); - }); + } + }; + + await pc.setRemoteDescription(new RTCSessionDescription(offer)); + + const answer = await pc.createAnswer(); + await pc.setLocalDescription(answer); + + this.sendSignal({ to: from, type: 'canvas_answer', answer: answer }); + } + + async handleCandidate(from: string, candidate: RTCIceCandidateInit): Promise { + const pc = this.connections.get(from); + if (pc) { + try { + await pc.addIceCandidate(new RTCIceCandidate(candidate)); + } catch (e) { + console.error('Error adding ICE candidate', e); + } + } } clear() { - if (this.peer) { - // otherwise it calls reconnection on data chan close - const peer = this.peer; - this.peer = null; - peer.disconnect(); - peer.destroy(); - } + this.connections.forEach((pc) => { + pc.close(); + }); + this.connections.clear(); + this.streams.clear(); } } diff --git a/frontend/package.json b/frontend/package.json index 7dad503e9..452b83529 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -52,7 +52,6 @@ "mobx": "^6.13.3", "mobx-persist-store": "^1.1.5", "mobx-react-lite": "^4.0.7", - "peerjs": "1.3.2", "prismjs": "^1.29.0", "rc-time-picker": "^3.7.3", "react": "^18.2.0", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index f1b91f871..acf5e8837 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2952,67 +2952,61 @@ __metadata: languageName: node linkType: hard -"@sentry/browser@npm:^5.21.1": - version: 5.30.0 - resolution: "@sentry/browser@npm:5.30.0" +"@sentry-internal/browser-utils@npm:8.55.0": + version: 8.55.0 + resolution: "@sentry-internal/browser-utils@npm:8.55.0" dependencies: - "@sentry/core": "npm:5.30.0" - "@sentry/types": "npm:5.30.0" - "@sentry/utils": "npm:5.30.0" - tslib: "npm:^1.9.3" - checksum: 10c1/4787cc3ea90600b36b548a8403afb30f13e1e562dd426871879d824536c16005d0734b7406498f1a6dd4fa7e0a49808e17a1c2c24750430ba7f86f909a9eb95a + "@sentry/core": "npm:8.55.0" + checksum: 10c1/67fdc5ec9c8bc6c8eeda4598332a7937a8c7d6cc1cadb05a886323f3d13c25def7b9258ad4b834919dea5d612010de8900f5cf738e9a577a711c839f285557d7 languageName: node linkType: hard -"@sentry/core@npm:5.30.0": - version: 5.30.0 - resolution: "@sentry/core@npm:5.30.0" +"@sentry-internal/feedback@npm:8.55.0": + version: 8.55.0 + resolution: "@sentry-internal/feedback@npm:8.55.0" dependencies: - "@sentry/hub": "npm:5.30.0" - "@sentry/minimal": "npm:5.30.0" - "@sentry/types": "npm:5.30.0" - "@sentry/utils": "npm:5.30.0" - tslib: "npm:^1.9.3" - checksum: 10c1/5c6dcdccc48a9d6957af7745226eacd3d4926574593e852ccbad0fbaa71355879b9c4707c194e3d9b1ef389d98171a3d85d2c75636a5c6d1cc3c9950cd06334a + "@sentry/core": "npm:8.55.0" + checksum: 10c1/3f6fd3b8c2305b457a5c729c92b2a2335200e5ee0d5a210b513246e00ecda6d2a28940871ed88eee7f7bd8465571388698a7b789c8e0f3d5832ff3a0b040b514 languageName: node linkType: hard -"@sentry/hub@npm:5.30.0": - version: 5.30.0 - resolution: "@sentry/hub@npm:5.30.0" +"@sentry-internal/replay-canvas@npm:8.55.0": + version: 8.55.0 + resolution: "@sentry-internal/replay-canvas@npm:8.55.0" dependencies: - "@sentry/types": "npm:5.30.0" - "@sentry/utils": "npm:5.30.0" - tslib: "npm:^1.9.3" - checksum: 10c1/28b86742c72427b5831ee3077c377d1f305d2eb080f7dc977e81b8f29e8eb0dfa07f129c1f5cda29bda9238fe50e292ab719119c4c5a5b7ef580a24bcb6356a3 + "@sentry-internal/replay": "npm:8.55.0" + "@sentry/core": "npm:8.55.0" + checksum: 10c1/48511881330193d754e01b842e3b2b931641d0954bac8a8f01503ff3d2aedc9f1779049be0a7a56ba35583769f0566381853c7656888c42f9f59224c6520e593 languageName: node linkType: hard -"@sentry/minimal@npm:5.30.0": - version: 5.30.0 - resolution: "@sentry/minimal@npm:5.30.0" +"@sentry-internal/replay@npm:8.55.0": + version: 8.55.0 + resolution: "@sentry-internal/replay@npm:8.55.0" dependencies: - "@sentry/hub": "npm:5.30.0" - "@sentry/types": "npm:5.30.0" - tslib: "npm:^1.9.3" - checksum: 10c1/d28ad14e43d3c5c06783288ace1fcf1474437070f04d1476b04d0288656351d9a6285cc66d346e8d84a3e73cf895944c06fa7c82bad93415831e4449e11f2d89 + "@sentry-internal/browser-utils": "npm:8.55.0" + "@sentry/core": "npm:8.55.0" + checksum: 10c1/d60b4261df037b4c82dafc6b25695b2be32f95a45cd25fc43c659d65644325238f7152f6222cd5d4f3f52407c3f5ad67ea30b38fea27c9422536f8aaba6b0048 languageName: node linkType: hard -"@sentry/types@npm:5.30.0": - version: 5.30.0 - resolution: "@sentry/types@npm:5.30.0" - checksum: 10c1/07fe7f04f6aae13f037761fe56a20e06fa4a768bf024fb81970d3087ab9ab5b45bd85b9081945ef5019d93b7de742918374a0e7b70a992dbb29a5078982ddfd9 +"@sentry/browser@npm:^8.34.0": + version: 8.55.0 + resolution: "@sentry/browser@npm:8.55.0" + dependencies: + "@sentry-internal/browser-utils": "npm:8.55.0" + "@sentry-internal/feedback": "npm:8.55.0" + "@sentry-internal/replay": "npm:8.55.0" + "@sentry-internal/replay-canvas": "npm:8.55.0" + "@sentry/core": "npm:8.55.0" + checksum: 10c1/3baf51a0b401bb63b345df480773d49b713dd557e15baf2c98f089612c9497aca6f2c7849b1c4d6ded6229d1de495e3305a0438145333de26c6ba190d261c039 languageName: node linkType: hard -"@sentry/utils@npm:5.30.0": - version: 5.30.0 - resolution: "@sentry/utils@npm:5.30.0" - dependencies: - "@sentry/types": "npm:5.30.0" - tslib: "npm:^1.9.3" - checksum: 10c1/311ad0be0e40af9f4ab7be2dfb8a782a779fa56700a0662f49ebcbff0dbbe4ea5dff690ad2c0ed4ecb6a6721a3066186b3c8f677fa302c5b606f86dfaa654de3 +"@sentry/core@npm:8.55.0": + version: 8.55.0 + resolution: "@sentry/core@npm:8.55.0" + checksum: 10c1/fbb71058626214674c4b103160fea859ce1fcc83b26533920b2c4fc7d5169bde178b08cd46dad29fabaf616fa465db4274356c500a37f33888bdb8d10fda3d55 languageName: node linkType: hard @@ -3479,13 +3473,6 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^10.14.33": - version: 10.17.60 - resolution: "@types/node@npm:10.17.60" - checksum: 10c1/63eca0b871718af75e369cd2a57e70939ff347f4b1fbd98dca40d005262ab48021e3829f58ed5e828d0886e98054cfa354648118712d28779eb4a0f855e70a93 - languageName: node - linkType: hard - "@types/parse-json@npm:^4.0.0": version: 4.0.2 resolution: "@types/parse-json@npm:4.0.2" @@ -7487,13 +7474,6 @@ __metadata: languageName: node linkType: hard -"eventemitter3@npm:^3.1.2": - version: 3.1.2 - resolution: "eventemitter3@npm:3.1.2" - checksum: 10c1/e177ace79a8f6d9884adad7d820f819132c5ec6e9d88e598043943f1951000901ad871f79dbd385fc980c7720b9f5644a2a12e95a767310a00401b304839f999 - languageName: node - linkType: hard - "eventemitter3@npm:^4.0.0, eventemitter3@npm:^4.0.1": version: 4.0.7 resolution: "eventemitter3@npm:4.0.7" @@ -11574,7 +11554,7 @@ __metadata: "@jest/globals": "npm:^29.7.0" "@medv/finder": "npm:^3.1.0" "@openreplay/sourcemap-uploader": "npm:^3.0.10" - "@sentry/browser": "npm:^5.21.1" + "@sentry/browser": "npm:^8.34.0" "@svg-maps/world": "npm:^1.0.1" "@tanstack/react-query": "npm:^5.56.2" "@trivago/prettier-plugin-sort-imports": "npm:^4.3.0" @@ -11633,7 +11613,6 @@ __metadata: mobx-persist-store: "npm:^1.1.5" mobx-react-lite: "npm:^4.0.7" node-gyp: "npm:^9.0.0" - peerjs: "npm:1.3.2" postcss: "npm:^8.4.48" postcss-import: "npm:^16.1.0" postcss-loader: "npm:^8.1.1" @@ -11964,25 +11943,6 @@ __metadata: languageName: node linkType: hard -"peerjs-js-binarypack@npm:1.0.1": - version: 1.0.1 - resolution: "peerjs-js-binarypack@npm:1.0.1" - checksum: 10c1/5670269e0a61400151f99727c53956c2971a6d5999a6ba4684b565cc3d4429b81fd04cb45b040677192d58537a79df1eadb2a7379cb1ffd0b04f01e6b292ef48 - languageName: node - linkType: hard - -"peerjs@npm:1.3.2": - version: 1.3.2 - resolution: "peerjs@npm:1.3.2" - dependencies: - "@types/node": "npm:^10.14.33" - eventemitter3: "npm:^3.1.2" - peerjs-js-binarypack: "npm:1.0.1" - webrtc-adapter: "npm:^7.7.1" - checksum: 10c1/fdbe1794fdab77b2d610ad1dd8fdf705aa4f128fc1c62cab861e8fb99a1255a71199701d6cd4a42d8a82047e4b59710848ff0fe1131aad3652d66665c0a8dffa - languageName: node - linkType: hard - "pend@npm:~1.2.0": version: 1.2.0 resolution: "pend@npm:1.2.0" @@ -14246,15 +14206,6 @@ __metadata: languageName: node linkType: hard -"rtcpeerconnection-shim@npm:^1.2.15": - version: 1.2.15 - resolution: "rtcpeerconnection-shim@npm:1.2.15" - dependencies: - sdp: "npm:^2.6.0" - checksum: 10c1/75b9e83fab08bb9eb90db61f05861db2a6a96f7abae9971359965b0cfd195a22227d8782b3b0fd6897e6a7501d63eee5fb3c92f030245cc421d2e745e99aaa7f - languageName: node - linkType: hard - "run-applescript@npm:^7.0.0": version: 7.0.0 resolution: "run-applescript@npm:7.0.0" @@ -14422,13 +14373,6 @@ __metadata: languageName: node linkType: hard -"sdp@npm:^2.12.0, sdp@npm:^2.6.0": - version: 2.12.0 - resolution: "sdp@npm:2.12.0" - checksum: 10c1/68ea1f4b58672b5a6e381bd08e4f0b66bc2a6f70b3dad06f05e0d44d4209e1ae7615159570f4f112179b65dbd855778b2b68ab44471cc0b28666335641332a2c - languageName: node - linkType: hard - "select-hose@npm:^2.0.0": version: 2.0.0 resolution: "select-hose@npm:2.0.0" @@ -15762,7 +15706,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^1.8.1, tslib@npm:^1.9.3": +"tslib@npm:^1.8.1": version: 1.14.1 resolution: "tslib@npm:1.14.1" checksum: 10c1/24ee51ea8fb127ca8ad30a25fdac22c5bb11a2b043781757ddde0daf2e03126e1e13e88ab1748d9c50f786a648b5b038e70782063fd15c3ad07ebec039df8f6f @@ -16487,16 +16431,6 @@ __metadata: languageName: node linkType: hard -"webrtc-adapter@npm:^7.7.1": - version: 7.7.1 - resolution: "webrtc-adapter@npm:7.7.1" - dependencies: - rtcpeerconnection-shim: "npm:^1.2.15" - sdp: "npm:^2.12.0" - checksum: 10c1/24330cf7be5f46599b333fbfdd54ac16c76c24c4c7744bcda9beed8aa77135924aaaf9f39e7844066b8c218a81dd13a6e0d4e221294d826572ec631b5750ea9a - languageName: node - linkType: hard - "websocket-driver@npm:>=0.5.1, websocket-driver@npm:^0.7.4": version: 0.7.4 resolution: "websocket-driver@npm:0.7.4" diff --git a/tracker/tracker-assist/package.json b/tracker/tracker-assist/package.json index 8745f6126..8a0609081 100644 --- a/tracker/tracker-assist/package.json +++ b/tracker/tracker-assist/package.json @@ -30,7 +30,6 @@ "dependencies": { "csstype": "^3.0.10", "fflate": "^0.8.2", - "peerjs": "1.5.4", "socket.io-client": "^4.8.1" }, "peerDependencies": { diff --git a/tracker/tracker-assist/src/Assist.ts b/tracker/tracker-assist/src/Assist.ts index 6fa987a81..2221f241f 100644 --- a/tracker/tracker-assist/src/Assist.ts +++ b/tracker/tracker-assist/src/Assist.ts @@ -1,25 +1,21 @@ /* eslint-disable @typescript-eslint/no-empty-function */ -import type { Socket, } from 'socket.io-client' -import { connect, } from 'socket.io-client' -import Peer, { MediaConnection, } from 'peerjs' -import type { Properties, } from 'csstype' -import { App, } from '@openreplay/tracker' +import type { Socket } from 'socket.io-client' +import { connect } from 'socket.io-client' +import type { Properties } from 'csstype' +import { App } from '@openreplay/tracker' -import RequestLocalStream, { LocalStream, } from './LocalStream.js' -import {hasTag,} from './guards.js' -import RemoteControl, { RCStatus, } from './RemoteControl.js' +import RequestLocalStream, { LocalStream } from './LocalStream.js' +import { hasTag } from './guards.js' +import RemoteControl, { RCStatus } from './RemoteControl.js' import CallWindow from './CallWindow.js' import AnnotationCanvas from './AnnotationCanvas.js' import ConfirmWindow from './ConfirmWindow/ConfirmWindow.js' -import { callConfirmDefault, } from './ConfirmWindow/defaults.js' -import type { Options as ConfirmOptions, } from './ConfirmWindow/defaults.js' +import { callConfirmDefault } from './ConfirmWindow/defaults.js' +import type { Options as ConfirmOptions } from './ConfirmWindow/defaults.js' import ScreenRecordingState from './ScreenRecordingState.js' -import { pkgVersion, } from './version.js' +import { pkgVersion } from './version.js' import Canvas from './Canvas.js' -import { gzip, } from 'fflate' -// TODO: fully specified strict check with no-any (everywhere) -// @ts-ignore -const safeCastedPeer = Peer.default || Peer +import { gzip } from 'fflate' type StartEndCallback = (agentInfo?: Record) => ((() => any) | void) @@ -52,26 +48,23 @@ export interface Options { confirmStyle?: Properties; config: RTCConfiguration; - serverURL: string + serverURL: string; callUITemplate?: string; compressionEnabled: boolean; /** * Minimum amount of messages in a batch to trigger compression run * @default 5000 - * */ - compressionMinBatchSize: number + */ + compressionMinBatchSize: number; } - enum CallingState { Requesting, True, False, } - -// TODO typing???? -type OptionalCallback = (()=>Record) | void +type OptionalCallback = (() => Record) | void type Agent = { onDisconnect?: OptionalCallback, onControlReleased?: OptionalCallback, @@ -84,8 +77,8 @@ export default class Assist { readonly version = pkgVersion private socket: Socket | null = null - private peer: Peer | null = null - private canvasPeers: Record = {} + private calls: Record = {}; + private canvasPeers: Record = {} private canvasNodeCheckers: Map = new Map() private assistDemandedRestart = false private callingState: CallingState = CallingState.False @@ -95,6 +88,10 @@ export default class Assist { private readonly options: Options private readonly canvasMap: Map = new Map() + // Для локального аудио/видео потока + private localStream: MediaStream | null = null; + private isCalling: boolean = false; + constructor( private readonly app: App, options?: Partial, @@ -103,20 +100,20 @@ export default class Assist { // @ts-ignore window.__OR_ASSIST_VERSION = this.version this.options = Object.assign({ - session_calling_peer_key: '__openreplay_calling_peer', - session_control_peer_key: '__openreplay_control_peer', - config: null, - serverURL: null, - onCallStart: ()=>{}, - onAgentConnect: ()=>{}, - onRemoteControlStart: ()=>{}, - callConfirm: {}, - controlConfirm: {}, // TODO: clear options passing/merging/overwriting - recordingConfirm: {}, - socketHost: '', - compressionEnabled: false, - compressionMinBatchSize: 5000, - }, + session_calling_peer_key: '__openreplay_calling_peer', + session_control_peer_key: '__openreplay_control_peer', + config: null, + serverURL: null, + onCallStart: () => { }, + onAgentConnect: () => { }, + onRemoteControlStart: () => { }, + callConfirm: {}, + controlConfirm: {}, // TODO: clear options passing/merging/overwriting + recordingConfirm: {}, + socketHost: '', + compressionEnabled: false, + compressionMinBatchSize: 5000, + }, options, ) @@ -155,7 +152,7 @@ export default class Assist { if (this.agentsConnected) { const batchSize = messages.length // @ts-ignore No need in statistics messages. TODO proper filter - if (batchSize === 2 && messages[0]._id === 0 && messages[1]._id === 49) { return } + if (batchSize === 2 && messages[0]._id === 0 && messages[1]._id === 49) { return } if (batchSize > this.options.compressionMinBatchSize && this.options.compressionEnabled) { const toSend: any[] = [] if (batchSize > 10000) { @@ -198,17 +195,17 @@ export default class Assist { private readonly setCallingState = (newState: CallingState): void => { this.callingState = newState } - private getHost():string{ + private getHost(): string { if (this.options.socketHost) { return this.options.socketHost } - if (this.options.serverURL){ + if (this.options.serverURL) { return new URL(this.options.serverURL).host } return this.app.getHost() } - private getBasePrefixUrl(): string{ - if (this.options.serverURL){ + private getBasePrefixUrl(): string { + if (this.options.serverURL) { return new URL(this.options.serverURL).pathname } return '' @@ -232,7 +229,7 @@ export default class Assist { // SocketIO const socket = this.socket = connect(this.getHost(), { - path: this.getBasePrefixUrl()+'/ws-assist/socket', + path: this.getBasePrefixUrl() + '/ws-assist/socket', query: { 'peerId': peerID, 'identity': 'session', @@ -258,13 +255,16 @@ export default class Assist { return } app.debug.log('Socket:', ...args) + socket.on('close', (e) => { + console.warn('Socket closed:', e); + }) }) const onGrand = (id: string) => { if (!callUI) { callUI = new CallWindow(app.debug.error, this.options.callUITemplate) } - if (this.remoteControl){ + if (this.remoteControl) { callUI?.showRemoteControl(this.remoteControl.releaseControl) } this.agents[id] = { ...this.agents[id], onControlReleased: this.options.onRemoteControlStart(this.agents[id]?.agentInfo), } @@ -274,26 +274,24 @@ export default class Assist { return callingAgents.get(id) } const onRelease = (id?: string | null, isDenied?: boolean) => { - { - if (id) { - const cb = this.agents[id].onControlReleased - delete this.agents[id].onControlReleased - typeof cb === 'function' && cb() - this.emit('control_rejected', id) - } - if (annot != null) { - annot.remove() - annot = null - } - callUI?.hideRemoteControl() - if (this.callingState !== CallingState.True) { - callUI?.remove() - callUI = null - } - if (isDenied) { - const info = id ? this.agents[id]?.agentInfo : {} - this.options.onRemoteControlDeny?.(info || {}) - } + if (id) { + const cb = this.agents[id].onControlReleased + delete this.agents[id].onControlReleased + typeof cb === 'function' && cb() + this.emit('control_rejected', id) + } + if (annot != null) { + annot.remove() + annot = null + } + callUI?.hideRemoteControl() + if (this.callingState !== CallingState.True) { + callUI?.remove() + callUI = null + } + if (isDenied) { + const info = id ? this.agents[id]?.agentInfo : {} + this.options.onRemoteControlDeny?.(info || {}) } } @@ -385,7 +383,7 @@ export default class Assist { this.app.allowAppStart() setTimeout(() => { this.app.start().then(() => { this.assistDemandedRestart = false }) - .then(() => { + .then(() => { this.remoteControl?.reconnect(ids) }) .catch(e => app.debug.error(e)) @@ -421,8 +419,8 @@ export default class Assist { const name = info.data callingAgents.set(id, name) - if (!this.peer) { - setupPeer() + if (!this.isCalling) { + setupCallSignaling(); } updateCallerNames() }) @@ -450,7 +448,6 @@ export default class Assist { const callingAgents: Map = new Map() // !! uses socket.io ID // TODO: merge peerId & socket.io id (simplest way - send peerId with the name) - const calls: Record = {} // !! uses peerJS ID const lStreams: Record = {} function updateCallerNames() { @@ -467,9 +464,9 @@ export default class Assist { } const handleCallEnd = () => { // Complete stop and clear all calls // Streams - Object.values(calls).forEach(call => call.close()) - Object.keys(calls).forEach(peerId => { - delete calls[peerId] + Object.values(this.calls).forEach(pc => pc.close()) + Object.keys(this.calls).forEach(peerId => { + delete this.calls[peerId] }) Object.values(lStreams).forEach((stream) => { stream.stop() }) Object.keys(lStreams).forEach((peerId: string) => { delete lStreams[peerId] }) @@ -484,7 +481,7 @@ export default class Assist { callUI?.hideControls() } - this.emit('UPDATE_SESSION', { agentIds: [], isCallActive: false, }) + this.emit('UPDATE_SESSION', { agentIds: [], isCallActive: false }) this.setCallingState(CallingState.False) sessionStorage.removeItem(this.options.session_calling_peer_key) @@ -498,166 +495,176 @@ export default class Assist { } } - // PeerJS call (todo: use native WebRTC) - const peerOptions = { - host: this.getHost(), - path: this.getBasePrefixUrl()+'/assist', - port: location.protocol === 'http:' && this.noSecureMode ? 80 : 443, - debug: 2, //appOptions.__debug_log ? 2 : 0, // 0 Print nothing //1 Prints only errors. / 2 Prints errors and warnings. / 3 Prints all logs. - } - const setupPeer = () => { - if (this.options.config) { - peerOptions['config'] = this.options.config - } - - const peer = new safeCastedPeer(peerID, peerOptions) as Peer - this.peer = peer - let peerReconnectAttempts = 0 - // @ts-ignore (peerjs typing) - peer.on('error', e => app.debug.warn('Peer error: ', e.type, e)) - peer.on('disconnected', () => { - if (peerReconnectAttempts < 30) { - this.peerReconnectTimeout = setTimeout(() => { - if (this.app.active() && !peer.destroyed) { - peer.reconnect() - } - }, Math.min(peerReconnectAttempts, 8) * 2 * 1000) - peerReconnectAttempts += 1 - } - }) - - - const requestCallConfirm = () => { - if (callConfirmAnswer) { // Already asking - return callConfirmAnswer - } - callConfirmWindow = new ConfirmWindow(callConfirmDefault(this.options.callConfirm || { - text: this.options.confirmText, - style: this.options.confirmStyle, - })) // TODO: reuse ? - return callConfirmAnswer = callConfirmWindow.mount().then(answer => { - closeCallConfirmWindow() - return answer - }) - } - - const initiateCallEnd = () => { - this.emit('call_end') - handleCallEnd() - } - const updateVideoFeed = ({ enabled, }) => this.emit('videofeed', { streamId: this.peer?.id, enabled, }) - - peer.on('call', (call) => { - app.debug.log('Incoming call from', call.peer) - let confirmAnswer: Promise - const callingPeerIds = JSON.parse(sessionStorage.getItem(this.options.session_calling_peer_key) || '[]') - if (callingPeerIds.includes(call.peer) || this.callingState === CallingState.True) { - confirmAnswer = Promise.resolve(true) - } else { - this.setCallingState(CallingState.Requesting) - confirmAnswer = requestCallConfirm() - this.playNotificationSound() // For every new agent during confirmation here - - // TODO: only one (latest) timeout - setTimeout(() => { - if (this.callingState !== CallingState.Requesting) { return } - initiateCallEnd() - }, 30000) - } - - confirmAnswer.then(async agreed => { - if (!agreed) { - initiateCallEnd() - this.options.onCallDeny?.() - return - } - // Request local stream for the new connection + const setupCallSignaling = () => { + console.log("SETUP CALL 2"); + socket.on('webrtc_call_offer', async (_, data: { from: string, offer: RTCSessionDescriptionInit }) => { + console.log('Incoming call offer from', data, data.from, data.offer); + await handleIncomingCallOffer(data.from, data.offer); + }); + socket.on('webrtc_call_answer', async (data: { from: string, answer: RTCSessionDescriptionInit }) => { + const pc = this.calls[data.from]; + if (pc) { try { - // lStreams are reusable so fare we don't delete them in the `endAgentCall` - if (!lStreams[call.peer]) { - app.debug.log('starting new stream for', call.peer) - lStreams[call.peer] = await RequestLocalStream() - } - calls[call.peer] = call + await pc.setRemoteDescription(new RTCSessionDescription(data.answer)); } catch (e) { - app.debug.error('Audio media device request error:', e) - initiateCallEnd() + app.debug.error('Error setting remote description from answer', e); + } + } + }); + socket.on('webrtc_ice_candidate', async (data: { from: string, candidate: RTCIceCandidateInit }) => { + const pc = this.calls[data.from]; + if (pc) { + try { + await pc.addIceCandidate(new RTCIceCandidate(data.candidate)); + } catch (e) { + app.debug.error('Error adding ICE candidate', e); + } + } + }); + }; + + const handleIncomingCallOffer = async (from: string, offer: RTCSessionDescriptionInit) => { + app.debug.log('handleIncomingCallOffer', from) + let confirmAnswer: Promise + const callingPeerIds = JSON.parse(sessionStorage.getItem(this.options.session_calling_peer_key) || '[]') + if (callingPeerIds.includes(from) || this.callingState === CallingState.True) { + confirmAnswer = Promise.resolve(true) + } else { + this.setCallingState(CallingState.Requesting) + confirmAnswer = requestCallConfirm() + this.playNotificationSound() // For every new agent during confirmation here + + // TODO: only one (latest) timeout + setTimeout(() => { + if (this.callingState !== CallingState.Requesting) { return } + initiateCallEnd() + }, 30000) + } + + try { + const agreed = await confirmAnswer + if (!agreed) { + initiateCallEnd() + this.options.onCallDeny?.() + return + } + // Request local stream for the new connection + if (!lStreams[from]) { + app.debug.log('starting new stream for', from) + lStreams[from] = await RequestLocalStream() + } + const pc = new RTCPeerConnection(this.options.config); + lStreams[from].stream.getTracks().forEach(track => { + pc.addTrack(track, lStreams[from].stream); + }); + // Обработка ICE-кандидатов + console.log("should generate ice"); + + pc.onicecandidate = (event) => { + console.log("GENERATING ICE CANDIDATE", event); + if (event.candidate) { + socket.emit('webrtc_ice_candidate', { to: from, candidate: event.candidate }); + } + }; + // Обработка входящего медиапотока + pc.ontrack = (event) => { + const rStream = event.streams[0]; + if (rStream && callUI) { + callUI.addRemoteStream(rStream, from); + const onInteraction = () => { + callUI?.playRemote(); + document.removeEventListener('click', onInteraction); + }; + document.addEventListener('click', onInteraction); + } + }; + // Сохраняем соединение + this.calls[from] = pc; + // устанавливаем remote description, создаём answer + console.log('1111111', offer); + await pc.setRemoteDescription(new RTCSessionDescription(offer)); + console.log('2222222'); + const answer = await pc.createAnswer(); + await pc.setLocalDescription(answer); + socket.emit('webrtc_call_answer', { to: from, answer }); + if (!callUI) { + callUI = new CallWindow(app.debug.error, this.options.callUITemplate) + callUI.setVideoToggleCallback((args: { enabled: boolean }) => + this.emit('videofeed', { streamId: from, enabled: args.enabled }) + ); + } + callUI.showControls(initiateCallEnd) + if (!annot) { + annot = new AnnotationCanvas() + annot.mount() + } + callUI.setLocalStreams(Object.values(lStreams)) + // Обработка ошибок соединения + pc.onconnectionstatechange = () => { + if (pc.connectionState === 'disconnected' || pc.connectionState === 'failed') { + initiateCallEnd(); + } + }; + // Обновление трека при изменении локального видео + lStreams[from].onVideoTrack(vTrack => { + const sender = pc.getSenders().find(s => s.track?.kind === 'video'); + if (!sender) { + app.debug.warn('No video sender found') return } - - if (!callUI) { - callUI = new CallWindow(app.debug.error, this.options.callUITemplate) - callUI.setVideoToggleCallback(updateVideoFeed) - } - callUI.showControls(initiateCallEnd) - - if (!annot) { - annot = new AnnotationCanvas() - annot.mount() - } - // have to be updated - callUI.setLocalStreams(Object.values(lStreams)) - - call.on('error', e => { - app.debug.warn('Call error:', e) - initiateCallEnd() - }) - call.on('stream', (rStream) => { - callUI?.addRemoteStream(rStream, call.peer) - const onInteraction = () => { // do only if document.hidden ? - callUI?.playRemote() - document.removeEventListener('click', onInteraction) - } - document.addEventListener('click', onInteraction) - }) - - // remote video on/off/camera change - lStreams[call.peer].onVideoTrack(vTrack => { - const sender = call.peerConnection.getSenders().find(s => s.track?.kind === 'video') - if (!sender) { - app.debug.warn('No video sender found') - return - } - app.debug.log('sender found:', sender) - void sender.replaceTrack(vTrack) - }) - - call.answer(lStreams[call.peer].stream) - document.addEventListener('visibilitychange', () => { - initiateCallEnd() - }) - - this.setCallingState(CallingState.True) - if (!callEndCallback) { callEndCallback = this.options.onCallStart?.() } - - const callingPeerIds = Object.keys(calls) - sessionStorage.setItem(this.options.session_calling_peer_key, JSON.stringify(callingPeerIds)) - this.emit('UPDATE_SESSION', { agentIds: callingPeerIds, isCallActive: true, }) - }).catch(reason => { // in case of Confirm.remove() without user answer (not an error) - app.debug.log(reason) + sender.replaceTrack(vTrack) }) - }) - } + document.addEventListener('visibilitychange', () => { + initiateCallEnd() + }) + this.setCallingState(CallingState.True) + if (!callEndCallback) { callEndCallback = this.options.onCallStart?.() } + const callingPeerIdsNow = Object.keys(this.calls) + sessionStorage.setItem(this.options.session_calling_peer_key, JSON.stringify(callingPeerIdsNow)) + this.emit('UPDATE_SESSION', { agentIds: callingPeerIdsNow, isCallActive: true }) + } catch (reason) { + app.debug.log(reason); + } + }; + // Функции запроса подтверждения, завершения вызова, уведомления и т.д. + const requestCallConfirm = () => { + if (callConfirmAnswer) { // Если уже запрошено подтверждение + return callConfirmAnswer; + } + callConfirmWindow = new ConfirmWindow(callConfirmDefault(this.options.callConfirm || { + text: this.options.confirmText, + style: this.options.confirmStyle, + })); + return callConfirmAnswer = callConfirmWindow.mount().then(answer => { + closeCallConfirmWindow(); + return answer; + }); + }; + + const initiateCallEnd = () => { + this.emit('call_end'); + handleCallEnd(); + }; const startCanvasStream = (stream: MediaStream, id: number) => { - const canvasPID = `${app.getProjectKey()}-${sessionId}-${id}` + const canvasPID = `${app.getProjectKey()}-${sessionId}-${id}`; if (!this.canvasPeers[id]) { - this.canvasPeers[id] = new safeCastedPeer(canvasPID, peerOptions) as Peer + this.canvasPeers[id] = new RTCPeerConnection(this.options.config); } - this.canvasPeers[id]?.on('error', (e) => app.debug.error(e)) - + const pc = this.canvasPeers[id]; + pc.onicecandidate = (event) => { + if (event.candidate) { + // Добавить отправку ICE-кандидата через socket + } + }; Object.values(this.agents).forEach(agent => { if (agent.agentInfo) { - const target = `${agent.agentInfo.peerId}-${agent.agentInfo.id}-canvas` - const connection = this.canvasPeers[id]?.connect(target) - connection?.on('open', () => { - if (agent.agentInfo) { - const call = this.canvasPeers[id]?.call(target, stream.clone()) - call?.on('error', app.debug.error) - } - }) - connection?.on('error', (e) => app.debug.error(e)) + // реализовать сигналинг для canvas чтобы агент создал свой RTCPeerConnection для canvas + stream.getTracks().forEach(track => { + pc.addTrack(track, stream); + }); + } else { app.debug.error('Assist: cant establish canvas peer to agent, no agent info') } @@ -686,14 +693,16 @@ export default class Assist { if (!isPresent) { canvasHandler.stop() this.canvasMap.delete(id) - this.canvasPeers[id]?.destroy() - this.canvasPeers[id] = null + if (this.canvasPeers[id]) { + this.canvasPeers[id]?.close() + this.canvasPeers[id] = null + } clearInterval(int) } }, 5000) this.canvasNodeCheckers.set(id, int) } - }) + }); } private playNotificationSound() { @@ -708,21 +717,19 @@ export default class Assist { private clean() { // sometimes means new agent connected, so we keep id for control - this.remoteControl?.releaseControl(false, true) + this.remoteControl?.releaseControl(false, true); if (this.peerReconnectTimeout) { clearTimeout(this.peerReconnectTimeout) this.peerReconnectTimeout = null } - if (this.peer) { - this.peer.destroy() - this.app.debug.log('Peer destroyed') - } + Object.values(this.calls).forEach(pc => pc.close()) + this.calls = {} if (this.socket) { this.socket.disconnect() this.app.debug.log('Socket disconnected') } this.canvasMap.clear() - this.canvasPeers = [] + this.canvasPeers = {} this.canvasNodeCheckers.forEach((int) => clearInterval(int)) this.canvasNodeCheckers.clear() } diff --git a/tracker/tracker-assist/src/RemoteControl.ts b/tracker/tracker-assist/src/RemoteControl.ts index 67e6b39d6..26b8e93a0 100644 --- a/tracker/tracker-assist/src/RemoteControl.ts +++ b/tracker/tracker-assist/src/RemoteControl.ts @@ -63,10 +63,10 @@ export default class RemoteControl { this.releaseControl(true) } }) - .then(() => { - this.confirm?.remove() - }) - .catch(e => { + .then(() => { + this.confirm?.remove() + }) + .catch(e => { this.confirm?.remove() console.error(e) }) @@ -113,7 +113,7 @@ export default class RemoteControl { scroll = (id, d) => { id === this.agentID && this.mouse?.scroll(d) } move = (id, xy) => { - return id === this.agentID && this.mouse?.move(xy) + return id === this.agentID && this.mouse?.move(xy) } private focused: HTMLElement | null = null click = (id, xy) => { diff --git a/tracker/tracker-redux/package.json b/tracker/tracker-redux/package.json index acd5509a8..9234e60be 100644 --- a/tracker/tracker-redux/package.json +++ b/tracker/tracker-redux/package.json @@ -28,14 +28,15 @@ "redux": "^4.0.0" }, "devDependencies": { + "@babel/core": "^7.26.8", "@openreplay/tracker": "file:../tracker", - "prettier": "^1.18.2", - "replace-in-files-cli": "^1.0.0", - "typescript": "^4.6.0-dev.20211126", "@rollup/plugin-babel": "^6.0.4", "@rollup/plugin-node-resolve": "^15.2.3", + "prettier": "^1.18.2", "replace-in-files": "^3.0.0", + "replace-in-files-cli": "^1.0.0", "rollup": "^4.14.0", - "rollup-plugin-terser": "^7.0.2" + "rollup-plugin-terser": "^7.0.2", + "typescript": "^4.6.0-dev.20211126" } }