Реализация нового протокола звонков

This commit is contained in:
RoyceDa
2026-04-04 16:48:26 +02:00
parent 3492a881cc
commit c052fdae41
3 changed files with 87 additions and 93 deletions

View File

@@ -16,7 +16,6 @@ import { attachReceiverE2EE, attachSenderE2EE } from "./audioE2EE";
import { useDeattachedSender } from "../DialogProvider/useDeattachedSender"; import { useDeattachedSender } from "../DialogProvider/useDeattachedSender";
import { AttachmentType } from "../ProtocolProvider/protocol/packets/packet.message"; import { AttachmentType } from "../ProtocolProvider/protocol/packets/packet.message";
import { generateRandomKey } from "@/app/utils/utils"; import { generateRandomKey } from "@/app/utils/utils";
import { useSystemInformation } from "../SystemProvider/useSystemInformation";
export interface CallContextValue { export interface CallContextValue {
call: (callable: string) => void; call: (callable: string) => void;
@@ -86,7 +85,12 @@ export function CallProvider(props : CallProviderProps) {
const soundRef = useRef<boolean>(true); const soundRef = useRef<boolean>(true);
const {sendMessage} = useDeattachedSender(); const {sendMessage} = useDeattachedSender();
const hasRemoteTrackRef = useRef<boolean>(false); const hasRemoteTrackRef = useRef<boolean>(false);
const systemInformation = useSystemInformation();
/**
* Используются для входа в звонок
*/
const callSessionIdRef = useRef<string>("");
const callTokenRef = useRef<string>("");
const {playSound, stopSound, stopLoopSound} = useSound(); const {playSound, stopSound, stopLoopSound} = useSound();
const {setWindowPriority} = useWindow(); const {setWindowPriority} = useWindow();
@@ -196,8 +200,6 @@ export function CallProvider(props : CallProviderProps) {
let answerSignal = new PacketWebRTC(); let answerSignal = new PacketWebRTC();
answerSignal.setSignalType(WebRTCSignalType.ANSWER); answerSignal.setSignalType(WebRTCSignalType.ANSWER);
answerSignal.setSdpOrCandidate(JSON.stringify(answer)); answerSignal.setSdpOrCandidate(JSON.stringify(answer));
answerSignal.setPublicKey(publicKey);
answerSignal.setDeviceId(systemInformation.id);
send(answerSignal); send(answerSignal);
info("Received WebRTC offer, set remote description and sent answer"); info("Received WebRTC offer, set remote description and sent answer");
return; return;
@@ -247,47 +249,49 @@ export function CallProvider(props : CallProviderProps) {
info("Received incoming call from " + packet.getSrc() + " but we are already on a call, sent busy signal"); info("Received incoming call from " + packet.getSrc() + " but we are already on a call, sent busy signal");
return; return;
} }
callSessionIdRef.current = packet.getCallId();
callTokenRef.current = packet.getJoinToken();
setWindowPriority(true); setWindowPriority(true);
playSound("ringtone.mp3", true); playSound("ringtone.mp3", true);
setActiveCall(packet.getSrc()); setActiveCall(packet.getSrc());
setCallState(CallState.INCOMING); setCallState(CallState.INCOMING);
setShowCallView(true); setShowCallView(true);
} }
if(signalType == SignalType.KEY_EXCHANGE && roleRef.current == CallRole.CALLER){ // if(signalType == SignalType.KEY_EXCHANGE && roleRef.current == CallRole.CALLER){
console.info("EXCHANGE SIGNAL RECEIVED, CALLER ROLE"); // console.info("EXCHANGE SIGNAL RECEIVED, CALLER ROLE");
/** // /**
* Другая сторона сгенерировала ключи для сессии и отправила нам публичную часть, // * Другая сторона сгенерировала ключи для сессии и отправила нам публичную часть,
* теперь мы можем создать общую секретную сессию для шифрования звонка // * теперь мы можем создать общую секретную сессию для шифрования звонка
*/ // */
const sharedPublic = packet.getSharedPublic(); // const sharedPublic = packet.getSharedPublic();
if(!sharedPublic){ // if(!sharedPublic){
info("Received key exchange signal without shared public key"); // info("Received key exchange signal without shared public key");
return; // return;
} // }
const sessionKeys = generateSessionKeys(); // const sessionKeys = generateSessionKeys();
const computedSharedSecret = nacl.box.before(Buffer.from(sharedPublic, 'hex'), sessionKeys.secretKey); // const computedSharedSecret = nacl.box.before(Buffer.from(sharedPublic, 'hex'), sessionKeys.secretKey);
sharedSecretRef.current = Buffer.from(computedSharedSecret).toString('hex'); // sharedSecretRef.current = Buffer.from(computedSharedSecret).toString('hex');
info("Generated shared secret for call session: " + sharedSecretRef.current); // info("Generated shared secret for call session: " + sharedSecretRef.current);
/** // /**
* Нам нужно отправить свой публичный ключ другой стороне, чтобы она тоже могла создать общую секретную сессию // * Нам нужно отправить свой публичный ключ другой стороне, чтобы она тоже могла создать общую секретную сессию
*/ // */
const signalPacket = new PacketSignalPeer(); // const signalPacket = new PacketSignalPeer();
signalPacket.setSrc(publicKey); // signalPacket.setSrc(publicKey);
signalPacket.setDst(packet.getSrc()); // signalPacket.setDst(packet.getSrc());
signalPacket.setSignalType(SignalType.KEY_EXCHANGE); // signalPacket.setSignalType(SignalType.KEY_EXCHANGE);
signalPacket.setSharedPublic(Buffer.from(sessionKeys.publicKey).toString('hex')); // signalPacket.setSharedPublic(Buffer.from(sessionKeys.publicKey).toString('hex'));
send(signalPacket); // send(signalPacket);
setCallState(CallState.WEB_RTC_EXCHANGE); // setCallState(CallState.WEB_RTC_EXCHANGE);
/** // /**
* Создаем комнату на сервере SFU, комнату создает звонящий // * Создаем комнату на сервере SFU, комнату создает звонящий
*/ // */
let webRtcSignal = new PacketSignalPeer(); // // let webRtcSignal = new PacketSignalPeer();
webRtcSignal.setSignalType(SignalType.CREATE_ROOM); // // webRtcSignal.setSignalType(SignalType.CREATE_ROOM);
webRtcSignal.setSrc(publicKey); // // webRtcSignal.setSrc(publicKey);
webRtcSignal.setDst(packet.getSrc()); // // webRtcSignal.setDst(packet.getSrc());
send(webRtcSignal); // // send(webRtcSignal);
} // }
if(signalType == SignalType.KEY_EXCHANGE && roleRef.current == CallRole.CALLEE){ if(signalType == SignalType.KEY_EXCHANGE){
console.info("EXCHANGE SIGNAL RECEIVED, CALLEE ROLE"); console.info("EXCHANGE SIGNAL RECEIVED, CALLEE ROLE");
/** /**
* Мы отправили свою публичную часть ключа другой стороне, * Мы отправили свою публичную часть ключа другой стороне,
@@ -308,7 +312,19 @@ export function CallProvider(props : CallProviderProps) {
info("Generated shared secret for call session: " + sharedSecretRef.current); info("Generated shared secret for call session: " + sharedSecretRef.current);
setCallState(CallState.WEB_RTC_EXCHANGE); setCallState(CallState.WEB_RTC_EXCHANGE);
} }
if(signalType == SignalType.CREATE_ROOM) { if(signalType == SignalType.ACCEPT){
/**
* Другая сторона приняла наш звонок, комната на SFU создалась, нужно сгенерировать ключи
*/
const keys = generateSessionKeys();
const signalPacket = new PacketSignalPeer();
signalPacket.setSrc(publicKey);
signalPacket.setDst(activeCall);
signalPacket.setSignalType(SignalType.KEY_EXCHANGE);
signalPacket.setSharedPublic(Buffer.from(keys.publicKey).toString('hex'));
send(signalPacket);
}
if(signalType == SignalType.ACTIVE) {
if(!sessionKeys){ if(!sessionKeys){
/** /**
* Сервер может отправить CREATE_ROOM сигнал, даже если мы приняли звонок на другом устройстве, по этому проверяем, * Сервер может отправить CREATE_ROOM сигнал, даже если мы приняли звонок на другом устройстве, по этому проверяем,
@@ -319,12 +335,6 @@ export function CallProvider(props : CallProviderProps) {
end(); end();
return; return;
} }
/**
* Создана комната для обмена WebRTC потоками, но такое событие сервер может отправить даже если звонок
* был принят с другого устройства, по этому проверяем, наш ли звонок
*/
roomIdRef.current = packet.getRoomId();
info("WebRTC room created with id: " + packet.getRoomId());
/** /**
* Нужно отправить свой SDP оффер другой стороне, чтобы установить WebRTC соединение * Нужно отправить свой SDP оффер другой стороне, чтобы установить WebRTC соединение
*/ */
@@ -341,8 +351,6 @@ export function CallProvider(props : CallProviderProps) {
let candidateSignal = new PacketWebRTC(); let candidateSignal = new PacketWebRTC();
candidateSignal.setSignalType(WebRTCSignalType.ICE_CANDIDATE); candidateSignal.setSignalType(WebRTCSignalType.ICE_CANDIDATE);
candidateSignal.setSdpOrCandidate(JSON.stringify(event.candidate)); candidateSignal.setSdpOrCandidate(JSON.stringify(event.candidate));
candidateSignal.setPublicKey(publicKey);
candidateSignal.setDeviceId(systemInformation.id);
send(candidateSignal); send(candidateSignal);
} }
} }
@@ -404,8 +412,6 @@ export function CallProvider(props : CallProviderProps) {
let offerSignal = new PacketWebRTC(); let offerSignal = new PacketWebRTC();
offerSignal.setSignalType(WebRTCSignalType.OFFER); offerSignal.setSignalType(WebRTCSignalType.OFFER);
offerSignal.setSdpOrCandidate(JSON.stringify(offer)); offerSignal.setSdpOrCandidate(JSON.stringify(offer));
offerSignal.setPublicKey(publicKey);
offerSignal.setDeviceId(systemInformation.id);
send(offerSignal); send(offerSignal);
return; return;
} }
@@ -541,14 +547,14 @@ export function CallProvider(props : CallProviderProps) {
stopLoopSound(); stopLoopSound();
stopSound(); stopSound();
/** /**
* Звонок принят, генерируем ключи для сессии и отправляем их другой стороне для установления защищенного канала связи * Звонок принят, можно обмениваться ключами
*/ */
const keys = generateSessionKeys();
const signalPacket = new PacketSignalPeer(); const signalPacket = new PacketSignalPeer();
signalPacket.setSrc(publicKey); signalPacket.setSrc(publicKey);
signalPacket.setDst(activeCall); signalPacket.setDst(activeCall);
signalPacket.setSignalType(SignalType.KEY_EXCHANGE); signalPacket.setCallId(callSessionIdRef.current);
signalPacket.setSharedPublic(Buffer.from(keys.publicKey).toString('hex')); signalPacket.setJoinToken(callTokenRef.current);
signalPacket.setSignalType(SignalType.ACCEPT);
send(signalPacket); send(signalPacket);
setCallState(CallState.KEY_EXCHANGE); setCallState(CallState.KEY_EXCHANGE);
roleRef.current = CallRole.CALLEE; roleRef.current = CallRole.CALLEE;

View File

@@ -6,9 +6,13 @@ export enum SignalType {
KEY_EXCHANGE = 1, KEY_EXCHANGE = 1,
ACTIVE_CALL = 2, ACTIVE_CALL = 2,
END_CALL = 3, END_CALL = 3,
CREATE_ROOM = 4, /**
* Переведен в стадию активного, значит комната на SFU уже создана и можно начинать обмен сигналами WebRTC
*/
ACTIVE = 4,
END_CALL_BECAUSE_PEER_DISCONNECTED = 5, END_CALL_BECAUSE_PEER_DISCONNECTED = 5,
END_CALL_BECAUSE_BUSY = 6 END_CALL_BECAUSE_BUSY = 6,
ACCEPT = 7
} }
/** /**
@@ -28,12 +32,8 @@ export class PacketSignalPeer extends Packet {
private signalType: SignalType = SignalType.CALL; private signalType: SignalType = SignalType.CALL;
/** private callId: string = "";
* Используется если SignalType == CREATE_ROOM, private joinToken: string = "";
* для идентификации комнаты на SFU сервере, в которой будет происходить обмен сигналами
* WebRTC для установления P2P соединения между участниками звонка
*/
private roomId: string = "";
public getPacketId(): number { public getPacketId(): number {
@@ -50,8 +50,9 @@ export class PacketSignalPeer extends Packet {
if(this.signalType == SignalType.KEY_EXCHANGE){ if(this.signalType == SignalType.KEY_EXCHANGE){
this.sharedPublic = stream.readString(); this.sharedPublic = stream.readString();
} }
if(this.signalType == SignalType.CREATE_ROOM){ if(this.signalType == SignalType.CALL || this.signalType == SignalType.ACCEPT){
this.roomId = stream.readString(); this.callId = stream.readString();
this.joinToken = stream.readString();
} }
} }
@@ -67,8 +68,9 @@ export class PacketSignalPeer extends Packet {
if(this.signalType == SignalType.KEY_EXCHANGE){ if(this.signalType == SignalType.KEY_EXCHANGE){
stream.writeString(this.sharedPublic); stream.writeString(this.sharedPublic);
} }
if(this.signalType == SignalType.CREATE_ROOM){ if(this.signalType == SignalType.CALL || this.signalType == SignalType.ACCEPT){
stream.writeString(this.roomId); stream.writeString(this.callId);
stream.writeString(this.joinToken);
} }
return stream; return stream;
} }
@@ -105,12 +107,20 @@ export class PacketSignalPeer extends Packet {
this.src = src; this.src = src;
} }
public getRoomId(): string { public getCallId(): string {
return this.roomId; return this.callId;
} }
public setRoomId(roomId: string) { public setCallId(callId: string) {
this.roomId = roomId; this.callId = callId;
}
public getJoinToken(): string {
return this.joinToken;
}
public setJoinToken(joinToken: string) {
this.joinToken = joinToken;
} }
} }

View File

@@ -15,8 +15,6 @@ export class PacketWebRTC extends Packet {
private signalType: WebRTCSignalType = WebRTCSignalType.OFFER; private signalType: WebRTCSignalType = WebRTCSignalType.OFFER;
private sdpOrCandidate: string = ""; private sdpOrCandidate: string = "";
private publicKey: string = "";
private deviceId: string = "";
public getPacketId(): number { public getPacketId(): number {
return 27; return 27;
@@ -25,8 +23,6 @@ export class PacketWebRTC extends Packet {
public _receive(stream: Stream): void { public _receive(stream: Stream): void {
this.signalType = stream.readInt8(); this.signalType = stream.readInt8();
this.sdpOrCandidate = stream.readString(); this.sdpOrCandidate = stream.readString();
this.publicKey = stream.readString();
this.deviceId = stream.readString();
} }
public _send(): Promise<Stream> | Stream { public _send(): Promise<Stream> | Stream {
@@ -34,8 +30,6 @@ export class PacketWebRTC extends Packet {
stream.writeInt16(this.getPacketId()); stream.writeInt16(this.getPacketId());
stream.writeInt8(this.signalType); stream.writeInt8(this.signalType);
stream.writeString(this.sdpOrCandidate); stream.writeString(this.sdpOrCandidate);
stream.writeString(this.publicKey);
stream.writeString(this.deviceId);
return stream; return stream;
} }
@@ -55,20 +49,4 @@ export class PacketWebRTC extends Packet {
return this.sdpOrCandidate; return this.sdpOrCandidate;
} }
public setPublicKey(key: string) {
this.publicKey = key;
}
public getPublicKey(): string {
return this.publicKey;
}
public setDeviceId(id: string) {
this.deviceId = id;
}
public getDeviceId(): string {
return this.deviceId;
}
} }