translated comments

This commit is contained in:
Андрей Бабушкин 2025-02-21 10:19:00 +01:00
parent 9aa8b6c5be
commit dec8e2d337
4 changed files with 77 additions and 97 deletions

View file

@ -33,7 +33,6 @@ export default class Call {
private peerID: string,
private getAssistVersion: () => number
) {
// Обработка событий сокета
socket.on('call_end', () => {
this.onRemoteCallEnd()
});
@ -55,7 +54,7 @@ export default class Call {
});
socket.on('messages_gz', () => {
if (reconnecting) {
// При восстановлении соединения инициируем повторное создание соединения
// When the connection is restored, we initiate a re-creation of the connection
this._callSessionPeer();
reconnecting = false;
}
@ -80,21 +79,21 @@ export default class Call {
this.assistVersion = this.getAssistVersion();
}
// СОЗДАНИЕ ЛОКАЛЬНОГО ПИРА
// CREATE A LOCAL PEER
private async createPeerConnection(remotePeerId: string): Promise<RTCPeerConnection> {
// создаем pc с конфигом ice
// create pc with ice config
const pc = new RTCPeerConnection({
iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
});
// Если есть локальный поток добавляем его треки в соединение
// If there is a local stream, add its tracks to the connection
if (this.callArgs && this.callArgs.localStream && this.callArgs.localStream.stream) {
this.callArgs.localStream.stream.getTracks().forEach((track) => {
pc.addTrack(track, this.callArgs!.localStream.stream);
});
}
// когда готов ice отсылваем его
// when ice is ready we send it
pc.onicecandidate = (event) => {
if (event.candidate) {
this.socket.emit('webrtc_call_ice_candidate', { from: remotePeerId, candidate: event.candidate });
@ -103,7 +102,7 @@ export default class Call {
}
};
// когда получаем удаленный трек записываем его в videoStreams[peerId]
// when we receive a remote track, we write it to videoStreams[peerId]
pc.ontrack = (event) => {
const stream = event.streams[0];
if (stream) {
@ -114,30 +113,17 @@ export default class Call {
if (this.callArgs) {
this.callArgs.onStream(stream);
}
try {
// Создаем элемент <video>
const video = document.createElement('video');
video.autoplay = true;
video.playsInline = true;
video.srcObject = stream;
// Добавляем <video> в <body>
document.body.appendChild(video);
} catch (error) {
console.error('Error accessing media devices:', error);
}
}
};
// Если связь отвалилась заканчиваем звонок
// If the connection is lost, we end the call
pc.onconnectionstatechange = () => {
if (pc.connectionState === "disconnected" || pc.connectionState === "failed") {
this.onRemoteCallEnd();
}
};
// Обработка замены трека при изменении локального видео
// Handle track replacement when local video changes
if (this.callArgs && this.callArgs.localStream) {
this.callArgs.localStream.onVideoTrack((vTrack: MediaStreamTrack) => {
const sender = pc.getSenders().find((s) => s.track?.kind === 'video');
@ -152,23 +138,23 @@ export default class Call {
return pc;
}
// УСТАНОВКА СОЕДИНЕНИЯ
// ESTABLISHING A CONNECTION
private async _peerConnection(remotePeerId: string) {
try {
// Создаём RTCPeerConnection
// Create RTCPeerConnection
const pc = await this.createPeerConnection(remotePeerId);
this.connections[remotePeerId] = pc;
// Создаём SDP offer
// Create an SDP offer
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// Отправляем offer
// Sending offer
this.socket.emit('webrtc_call_offer', { from: remotePeerId, offer });
this.connectAttempts = 0;
} catch (e: any) {
console.error(e);
// Пробуем переподключиться
// Trying to reconnect
const tryReconnect = async (error: any) => {
if (error.type === 'peer-unavailable' && this.connectAttempts < 5) {
this.connectAttempts++;
@ -184,18 +170,17 @@ export default class Call {
}
}
// Обрабатываем полученный answer на offer
// Process the received answer to offer
private async handleAnswer(data: { from: string, answer: RTCSessionDescriptionInit }) {
// устанавливаем в remotePeerId data.from
// set to remotePeerId data.from
const remotePeerId = data.from;
// получаем peer
const pc = this.connections[remotePeerId];
if (!pc) {
console.error("No connection found for remote peer", remotePeerId);
return;
}
try {
// если связь еще не установлена то устанвливаем remoteDescription в peer
// if the connection is not established yet, then set remoteDescription to peer
if (pc.signalingState !== "stable") {
await pc.setRemoteDescription(new RTCSessionDescription(data.answer));
} else {
@ -207,16 +192,14 @@ export default class Call {
}
}
// обрабатываем полученный iceCandidate
// process the received iceCandidate
private async handleIceCandidate(data: { from: string, candidate: RTCIceCandidateInit }) {
const remotePeerId = data.from;
// получаем peer
const pc = this.connections[remotePeerId];
if (!pc) return;
// если есть ice кандидаты
// if there are ice candidates then add candidate to peer
if (data.candidate && (data.candidate.sdpMid || data.candidate.sdpMLineIndex !== null)) {
try {
// добавляем кандидат в peer
await pc.addIceCandidate(new RTCIceCandidate(data.candidate));
} catch (e) {
console.error("Error adding ICE candidate", e);
@ -226,40 +209,40 @@ export default class Call {
}
}
// обрабатываем окончания звонка
// handle call ends
private handleCallEnd() {
// Если звонок не завершен, то вызываем onCallEnd
// If the call is not completed, then call onCallEnd
if (this.store.get().calling !== CallingState.NoCall) {
this.callArgs && this.callArgs.onCallEnd();
}
// меняем state на NoCall
// change state to NoCall
this.store.update({ calling: CallingState.NoCall });
// Закрываем все созданные RTCPeerConnection
// Close all created RTCPeerConnection
Object.values(this.connections).forEach((pc) => pc.close());
this.callArgs?.onCallEnd();
// Очищаем connections
// Clear connections
this.connections = {};
this.callArgs = null;
}
// Обработчик события завершения вызова по сигналу
// Call completion event handler by signal
private onRemoteCallEnd = () => {
if ([CallingState.Requesting, CallingState.Connecting].includes(this.store.get().calling)) {
// Если вызов еще не начался, то вызываем onReject
// If the call has not started yet, then call onReject
this.callArgs && this.callArgs.onReject();
// Закрываем все соединения и обнуляем callArgs
// Close all connections and reset callArgs
Object.values(this.connections).forEach((pc) => pc.close());
this.connections = {};
this.callArgs?.onCallEnd();
this.store.update({ calling: CallingState.NoCall });
this.callArgs = null;
} else {
// Вызываем полный обработчик завершения вызова
// Call the full call completion handler
this.handleCallEnd();
}
};
// Завершает вызов и отправляет сигнал call_end
// Ends the call and sends the call_end signal
initiateCallEnd = async () => {
const userName = userStore.account.name;
this.emitData('call_end', userName);
@ -300,9 +283,7 @@ export default class Call {
};
}
/**
* Инициирует вызов
*/
// Initiates a call
call(thirdPartyPeers?: string[]): { end: () => void } {
if (thirdPartyPeers && thirdPartyPeers.length > 0) {
this.addPeerCall(thirdPartyPeers);
@ -314,18 +295,17 @@ export default class Call {
};
}
// Уведомление пиров об изменении состояния локального видео
// Notify peers of local video state change
toggleVideoLocalStream(enabled: boolean) {
// Передаём сигнал через socket
this.emitData('videofeed', { streamId: this.peerID, enabled });
}
// Соединяемся с другими агентами
// Connect with other agents
addPeerCall(thirdPartyPeers: string[]) {
thirdPartyPeers.forEach((peerId) => this._peerConnection(peerId));
}
// Вызывает метод создания соединения с пиром
// Calls the method to create a connection with a peer
private _callSessionPeer() {
if (![CallingState.NoCall, CallingState.Reconnecting].includes(this.store.get().calling)) {
return;
@ -336,7 +316,7 @@ export default class Call {
console.warn('No tab data to connect to peer');
}
// Формируем идентификатор пира в зависимости от версии ассиста
// Generate a peer identifier depending on the assist version
const peerId =
this.getAssistVersion() === 1
? this.peerID
@ -347,7 +327,7 @@ export default class Call {
void this._peerConnection(peerId);
}
// Метод для очистки ресурсов
// Method for clearing resources
clean() {
void this.initiateCallEnd();
Object.values(this.connections).forEach((pc) => pc.close());

View file

@ -18,11 +18,11 @@ function draw(
export default class CanvasReceiver {
private streams: Map<string, MediaStream> = new Map();
// Храним RTCPeerConnection для каждого удалённого пира
// Store RTCPeerConnection for each remote peer
private connections: Map<string, RTCPeerConnection> = new Map();
private cId: string;
//sendSignal для отправки сигналов (offer/answer/ICE)
// sendSignal for sending signals (offer/answer/ICE)
constructor(
private readonly peerIdPrefix: string,
private readonly config: RTCIceServer[] | null,
@ -30,7 +30,7 @@ export default class CanvasReceiver {
private readonly agentInfo: Record<string, any>,
private readonly socket: Socket,
) {
// Формируем идентификатор как в PeerJS
// Form an id like in PeerJS
this.cId = `${this.peerIdPrefix}-${this.agentInfo.id}-canvas`;
this.socket.on('webrtc_canvas_offer', (data: { data: { offer: RTCSessionDescriptionInit, id: string }}) => {
@ -57,7 +57,7 @@ export default class CanvasReceiver {
iceServers: this.config ? this.config : [{ urls: "stun:stun.l.google.com:19302" }],
});
// Сохраняем соединение
// Save the connection
this.connections.set(id, pc);
pc.onicecandidate = (event) => {
@ -70,7 +70,7 @@ export default class CanvasReceiver {
const stream = event.streams[0];
if (stream) {
// Определяем canvasId из удалённого peer id
// Detect canvasId from remote peer id
const canvasId = id.split('-')[4];
this.streams.set(canvasId, stream);
setTimeout(() => {

View file

@ -153,7 +153,7 @@
</div>
<div id="video-container" class="card-body bg-dark p-0 d-flex align-items-center position-relative">
<div id="local-stream" class="ratio ratio-4x3 rounded m-0 p-0 shadow scale-x-[-1]">
<!-- пофиксить отображение по горизонтали -->
<!-- fix horizontal mirroring -->
<video id="video-local" autoplay muted class="scale-x-[-1]"></video>
</div>

View file

@ -449,7 +449,7 @@ export default class Assist {
}
})
// Если приходит videofeed то в ui показываем видео
// If a videofeed arrives, then we show the video in the ui
socket.on('videofeed', (_, info) => {
if (app.getTabId() !== info.meta.tabId) return
const feedState = info.data
@ -505,8 +505,12 @@ export default class Assist {
}
}
// обработка окончания вызова
// call end handling
const handleCallEnd = () => {
Object.values(this.calls).forEach(pc => pc.close())
this.calls = {}
Object.values(lStreams).forEach((stream) => { stream.stop() })
Object.keys(lStreams).forEach((peerId: string) => { delete lStreams[peerId] })
// UI
closeCallConfirmWindow()
if (this.remoteControl?.status === RCStatus.Disabled) {
@ -532,23 +536,22 @@ export default class Assist {
}
}
// обрабатываем входящий вызов
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) || '[]')
// если звонящий уже в списке, то сразу принимаем вызов без ui
// if the caller is already in the list, then we immediately accept the call without ui
if (callingPeerIds.includes(from) || this.callingState === CallingState.True) {
confirmAnswer = Promise.resolve(true)
} else {
// ставим стейт в ожидание подтверждения
// set the state to wait for confirmation
this.setCallingState(CallingState.Requesting)
// вызываем окно подтверждения вызова
// call the call confirmation window
confirmAnswer = requestCallConfirm()
// звуковое уведомление звонка
// sound notification of a call
this.playNotificationSound()
// через 30 сек сбрасываем вызов
// after 30 seconds we drop the call
setTimeout(() => {
if (this.callingState !== CallingState.Requesting) { return }
initiateCallEnd()
@ -556,7 +559,7 @@ export default class Assist {
}
try {
// ждем рещения по принятию вызова
// waiting for a decision on accepting the challenge
const agreed = await confirmAnswer
// если отказали, то завершаем вызов
if (!agreed) {
@ -564,14 +567,14 @@ export default class Assist {
this.options.onCallDeny?.()
return
}
// если приняли то чекаем ui, если окна вызова нет то создаем, привязываем toggle локального видео в тоглу через сокет
// if rejected, then terminate the call
if (!callUI) {
callUI = new CallWindow(app.debug.error, this.options.callUITemplate)
callUI.setVideoToggleCallback((args: { enabled: boolean }) =>
this.emit('videofeed', { streamId: from, enabled: args.enabled })
);
}
// показыаем кнопочки в окне вызова
// show buttons in the call window
callUI.showControls(initiateCallEnd)
if (!annot) {
annot = new AnnotationCanvas()
@ -580,38 +583,38 @@ export default class Assist {
// callUI.setLocalStreams(Object.values(lStreams))
try {
// если нет локальных стримов в lStrems то устанавливаем
// if there are no local streams in lStrems then we set
if (!lStreams[from]) {
app.debug.log('starting new stream for', from)
// запрашиваем локальный стрим, и устанавливаем в lStreams
// request a local stream, and set it to lStreams
lStreams[from] = await RequestLocalStream()
}
// полученные дорожки передаем в Call ui
// we pass the received tracks to Call ui
callUI.setLocalStreams(Object.values(lStreams))
} catch (e) {
app.debug.error('Error requesting local stream', e);
// если что-то не получилось то обрываем вызов
// if something didn't work out, we terminate the call
initiateCallEnd();
return;
}
// создаем новый RTCPeerConnection с конфигом ice серверов
// create a new RTCPeerConnection with ice server config
const pc = new RTCPeerConnection({
iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
});
// получаем все локальные треки и добавляем их в RTCPeerConnection
// get all local tracks and add them to RTCPeerConnection
lStreams[from].stream.getTracks().forEach(track => {
pc.addTrack(track, lStreams[from].stream);
});
// Когда получаем локальные ice кандидаты эмитим их через сокет
// When we receive local ice candidates, we emit them via socket
pc.onicecandidate = (event) => {
if (event.candidate) {
socket.emit('webrtc_call_ice_candidate', { from, candidate: event.candidate });
}
};
// когда получаем удаленный поток, добавляем его в call ui
// when we get a remote stream, add it to call ui
pc.ontrack = (event) => {
const rStream = event.streams[0];
if (rStream && callUI) {
@ -625,26 +628,26 @@ export default class Assist {
}
};
// Сохраняем соединение с звонящим
// Keep connection with the caller
this.calls[from] = pc;
// устанавливаем remote description на входящий запрос
// set remote description on incoming request
await pc.setRemoteDescription(new RTCSessionDescription(offer));
// создаем ответ на входящий запрос
// create a response to the incoming request
const answer = await pc.createAnswer();
// устанавливаем ответ как локальный
await pc.setLocalDescription(answer);
// передаем ответ
// set the response as local
socket.emit('webrtc_call_answer', { from, answer });
// Если меняется стейт на ощибку обрываем звонок
// If the state changes to an error, we terminate the call
pc.onconnectionstatechange = () => {
if (pc.connectionState === 'disconnected' || pc.connectionState === 'failed') {
initiateCallEnd();
}
};
// Обновление трека при изменении локального видео
// Update track when local video changes
lStreams[from].onVideoTrack(vTrack => {
const sender = pc.getSenders().find(s => s.track?.kind === 'video');
if (!sender) {
@ -654,16 +657,16 @@ export default class Assist {
sender.replaceTrack(vTrack)
})
// если пользователеь закрыл вкладку или переключился, то завершаем вызов
// if the user closed the tab or switched, then we end the call
document.addEventListener('visibilitychange', () => {
initiateCallEnd()
})
// когда все установилось то стейт переводим в true
// when everything is set, we change the state to true
this.setCallingState(CallingState.True)
if (!callEndCallback) { callEndCallback = this.options.onCallStart?.() }
const callingPeerIdsNow = Object.keys(this.calls)
// в session storage записываем всех с кем установлен вызов
// in session storage we write down everyone with whom the call is established
sessionStorage.setItem(this.options.session_calling_peer_key, JSON.stringify(callingPeerIdsNow))
this.emit('UPDATE_SESSION', { agentIds: callingPeerIdsNow, isCallActive: true })
} catch (reason) {
@ -671,9 +674,9 @@ export default class Assist {
}
};
// Функции запроса подтверждения, завершения вызова, уведомления и т.д.
// Functions for requesting confirmation, ending a call, notifying, etc.
const requestCallConfirm = () => {
if (callConfirmAnswer) { // Если уже запрошено подтверждение
if (callConfirmAnswer) { // If confirmation has already been requested
return callConfirmAnswer;
}
callConfirmWindow = new ConfirmWindow(callConfirmDefault(this.options.callConfirm || {
@ -686,15 +689,12 @@ export default class Assist {
});
};
// функция завершения вызова
const initiateCallEnd = () => {
this.emit('call_end');
handleCallEnd();
};
const startCanvasStream = async (stream: MediaStream, id: number) => {
// const canvasPID = `${app.getProjectKey()}-${sessionId}-${id}`;
// const target = `${agent.agentInfo.peerId}-${agent.agentInfo.id}-canvas`;
for (const agent of Object.values(this.agents)) {
if (!agent.agentInfo) return;
@ -710,11 +710,11 @@ export default class Assist {
this.canvasPeers[uniqueId]?.addTrack(track, stream);
});
// Создаем SDP offer
// Create SDP offer
const offer = await this.canvasPeers[uniqueId].createOffer();
await this.canvasPeers[uniqueId].setLocalDescription(offer);
// Отправляем offer через сервер сигналинга
// Send offer via signaling server
socket.emit('webrtc_canvas_offer', { offer, id: uniqueId });
}
@ -758,7 +758,7 @@ export default class Assist {
private setupPeerListeners(id: string) {
const peer = this.canvasPeers[id];
if (!peer) return;
// ICE-кандидаты
// ICE candidates
peer.onicecandidate = (event) => {
if (event.candidate && this.socket) {
this.socket.emit('webrtc_canvas_ice_candidate', {
@ -779,7 +779,7 @@ export default class Assist {
}
}
// очищаем все данные
// clear all data
private clean() {
// sometimes means new agent connected, so we keep id for control
this.remoteControl?.releaseControl(false, true);