resolved conflicts
This commit is contained in:
parent
6360b9a580
commit
07bbdf94ac
8 changed files with 2253 additions and 2006 deletions
|
|
@ -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<string, RTCPeerConnection> = {};
|
||||
private connectAttempts = 0;
|
||||
private videoStreams: Record<string, MediaStreamTrack> = {};
|
||||
|
||||
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<Peer> {
|
||||
if (this._peer && !this._peer.disconnected) {
|
||||
return Promise.resolve(this._peer);
|
||||
private async createPeerConnection(remotePeerId: string): Promise<RTCPeerConnection> {
|
||||
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 = {};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, MediaStream> = new Map();
|
||||
private peer: Peer | null = null;
|
||||
// Храним RTCPeerConnection для каждого удалённого пира
|
||||
private connections: Map<string, RTCPeerConnection> = 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<string, any>
|
||||
private readonly agentInfo: Record<string, any>,
|
||||
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<void> {
|
||||
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<void> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,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",
|
||||
|
|
|
|||
3343
frontend/yarn.lock
3343
frontend/yarn.lock
File diff suppressed because it is too large
Load diff
|
|
@ -30,7 +30,6 @@
|
|||
"dependencies": {
|
||||
"csstype": "^3.0.10",
|
||||
"fflate": "^0.8.2",
|
||||
"peerjs": "1.5.4",
|
||||
"socket.io-client": "^4.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
|
|
|||
|
|
@ -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<string, any>) => ((() => 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<string, unknown>) | void
|
||||
type OptionalCallback = (() => Record<string, unknown>) | 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<number, Peer | null> = {}
|
||||
private calls: Record<string, RTCPeerConnection> = {};
|
||||
private canvasPeers: Record<number, RTCPeerConnection | null> = {}
|
||||
private canvasNodeCheckers: Map<number, any> = 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<number, Canvas> = new Map()
|
||||
|
||||
// Для локального аудио/видео потока
|
||||
private localStream: MediaStream | null = null;
|
||||
private isCalling: boolean = false;
|
||||
|
||||
constructor(
|
||||
private readonly app: App,
|
||||
options?: Partial<Options>,
|
||||
|
|
@ -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<string, string> = new Map() // !! uses socket.io ID
|
||||
// TODO: merge peerId & socket.io id (simplest way - send peerId with the name)
|
||||
const calls: Record<string, MediaConnection> = {} // !! uses peerJS ID
|
||||
const lStreams: Record<string, LocalStream> = {}
|
||||
|
||||
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<boolean>
|
||||
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<boolean>
|
||||
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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue