diff --git a/app/providers/CallProvider/CallProvider.tsx b/app/providers/CallProvider/CallProvider.tsx index b153a51..985e182 100644 --- a/app/providers/CallProvider/CallProvider.tsx +++ b/app/providers/CallProvider/CallProvider.tsx @@ -1,11 +1,12 @@ import { Call } from "@/app/components/Call/Call"; import { useConsoleLogger } from "@/app/hooks/useConsoleLogger"; -import { createContext, useEffect, useRef, useState } from "react"; +import { createContext, useRef, useState } from "react"; import nacl from 'tweetnacl'; import { useSender } from "../ProtocolProvider/useSender"; -import { PacketSignal, SignalType } from "../ProtocolProvider/protocol/packets/packet.signal"; +import { PacketSignalPeer, SignalType } from "../ProtocolProvider/protocol/packets/packet.signal.peer"; import { usePacket } from "../ProtocolProvider/usePacket"; import { usePublicKey } from "../AccountProvider/usePublicKey"; +import { PacketWebRTC, WebRTCSignalType } from "../ProtocolProvider/protocol/packets/packet.webrtc"; export interface CallContextValue { @@ -64,15 +65,40 @@ export function CallProvider(props : CallProviderProps) { const send = useSender(); const publicKey = usePublicKey(); const peerConnectionRef = useRef(null); + const roomIdRef = useRef(""); const roleRef = useRef(null); const [sharedSecret, setSharedSecret] = useState(""); - useEffect(() => { - console.info("TRACE -> ", sharedSecret) - }, [sharedSecret]); + usePacket(27, async (packet: PacketWebRTC) => { + if(!activeCall || callState != CallState.WEB_RTC_EXCHANGE){ + /** + * Нет активного звонка или мы не на стадии обмена WebRTC сигналами, игнорируем + */ + return; + } + const signalType = packet.getSignalType(); + if(signalType == WebRTCSignalType.ANSWER){ + /** + * Другая сторона (сервер SFU) отправил нам SDP ответ на наш оффер + */ + const sdp = JSON.parse(packet.getSdpOrCandidate()); + await peerConnectionRef.current?.setRemoteDescription(new RTCSessionDescription(sdp)); + info("Received WebRTC answer and set remote description"); + return; + } + if(signalType == WebRTCSignalType.ICE_CANDIDATE){ + /** + * Другая сторона отправила нам ICE кандидата для установления WebRTC соединения + */ + const candidate = JSON.parse(packet.getSdpOrCandidate()); + await peerConnectionRef.current?.addIceCandidate(new RTCIceCandidate(candidate)); + info("Received WebRTC ICE candidate and added to peer connection"); + return; + } + }, [activeCall, sessionKeys, callState]); - usePacket(26, (packet: PacketSignal) => { + usePacket(26, async (packet: PacketSignalPeer) => { const signalType = packet.getSignalType(); if(activeCall){ /** @@ -123,13 +149,19 @@ export function CallProvider(props : CallProviderProps) { /** * Нам нужно отправить свой публичный ключ другой стороне, чтобы она тоже могла создать общую секретную сессию */ - const signalPacket = new PacketSignal(); + const signalPacket = new PacketSignalPeer(); signalPacket.setSrc(publicKey); signalPacket.setDst(packet.getSrc()); signalPacket.setSignalType(SignalType.KEY_EXCHANGE); signalPacket.setSharedPublic(Buffer.from(sessionKeys.publicKey).toString('hex')); send(signalPacket); setCallState(CallState.WEB_RTC_EXCHANGE); + /** + * Создаем комнату на сервере SFU, комнату создает звонящий + */ + let webRtcSignal = new PacketSignalPeer(); + webRtcSignal.setSignalType(SignalType.CREATE_ROOM); + send(webRtcSignal); } if(signalType == SignalType.KEY_EXCHANGE && roleRef.current == CallRole.CALLEE){ console.info("EXCHANGE SIGNAL RECEIVED, CALLEE ROLE"); @@ -152,6 +184,32 @@ export function CallProvider(props : CallProviderProps) { setSharedSecret(Buffer.from(computedSharedSecret).toString('hex')); setCallState(CallState.WEB_RTC_EXCHANGE); } + if(signalType == SignalType.CREATE_ROOM) { + /** + * Создана комната для обмена WebRTC потоками + */ + roomIdRef.current = packet.getRoomId(); + info("WebRTC room created with id: " + packet.getRoomId()); + /** + * Нужно отправить свой SDP оффер другой стороне, чтобы установить WebRTC соединение + */ + peerConnectionRef.current = new RTCPeerConnection({ + //Experemental + iceServers: [ + { urls: 'stun:stun.l.google.com:19302' } + ] + }); + /** + * Отправляем свой оффер другой стороне + */ + let offer = await peerConnectionRef.current.createOffer(); + peerConnectionRef.current.setLocalDescription(offer); + let offerSignal = new PacketWebRTC(); + offerSignal.setSignalType(WebRTCSignalType.OFFER); + offerSignal.setSdpOrCandidate(JSON.stringify(offer)); + send(offerSignal); + return; + } }, [activeCall, sessionKeys]); const generateSessionKeys = () => { @@ -165,7 +223,7 @@ export function CallProvider(props : CallProviderProps) { setActiveCall(dialog); setCallState(CallState.CONNECTING); setShowCallView(true); - const signalPacket = new PacketSignal(); + const signalPacket = new PacketSignalPeer(); signalPacket.setSrc(publicKey); signalPacket.setDst(dialog); signalPacket.setSignalType(SignalType.CALL); @@ -174,12 +232,13 @@ export function CallProvider(props : CallProviderProps) { } const close = () => { - const packetSignal = new PacketSignal(); + const packetSignal = new PacketSignalPeer(); packetSignal.setSrc(publicKey); packetSignal.setDst(activeCall); packetSignal.setSignalType(SignalType.END_CALL); send(packetSignal); peerConnectionRef.current = null; + roomIdRef.current = ""; setActiveCall(""); setCallState(CallState.ENDED); setShowCallView(false); @@ -199,7 +258,7 @@ export function CallProvider(props : CallProviderProps) { * Звонок принят, генерируем ключи для сессии и отправляем их другой стороне для установления защищенного канала связи */ const keys = generateSessionKeys(); - const signalPacket = new PacketSignal(); + const signalPacket = new PacketSignalPeer(); signalPacket.setSrc(publicKey); signalPacket.setDst(activeCall); signalPacket.setSignalType(SignalType.KEY_EXCHANGE); diff --git a/app/providers/ProtocolProvider/protocol/packets/packet.signal.peer.ts b/app/providers/ProtocolProvider/protocol/packets/packet.signal.peer.ts new file mode 100644 index 0000000..d37d513 --- /dev/null +++ b/app/providers/ProtocolProvider/protocol/packets/packet.signal.peer.ts @@ -0,0 +1,108 @@ +import Packet from "../packet"; +import Stream from "../stream"; + +export enum SignalType { + CALL = 0, + KEY_EXCHANGE = 1, + ACTIVE_CALL = 2, + END_CALL = 3, + CREATE_ROOM = 4 +} + +/** + * Пакет сигналинга, для сигналов WebRTC используется отдельный пакет 27 PacketWebRTCExchange + */ +export class PacketSignalPeer extends Packet { + + private src: string = ""; + /** + * Назначение + */ + private dst: string = ""; + /** + * Используется если SignalType == KEY_EXCHANGE, для идентификации сессии обмена ключами + */ + private sharedPublic: string = ""; + + private signalType: SignalType = SignalType.CALL; + + /** + * Используется если SignalType == CREATE_ROOM, + * для идентификации комнаты на SFU сервере, в которой будет происходить обмен сигналами + * WebRTC для установления P2P соединения между участниками звонка + */ + private roomId: string = ""; + + + public getPacketId(): number { + return 26; + } + + public _receive(stream: Stream): void { + this.signalType = stream.readInt8(); + this.src = stream.readString(); + this.dst = stream.readString(); + if(this.signalType == SignalType.KEY_EXCHANGE){ + this.sharedPublic = stream.readString(); + } + if(this.signalType == SignalType.CREATE_ROOM){ + this.roomId = stream.readString(); + } + } + + public _send(): Promise | Stream { + const stream = new Stream(); + stream.writeInt16(this.getPacketId()); + stream.writeInt8(this.signalType); + stream.writeString(this.src); + stream.writeString(this.dst); + if(this.signalType == SignalType.KEY_EXCHANGE){ + stream.writeString(this.sharedPublic); + } + if(this.signalType == SignalType.CREATE_ROOM){ + stream.writeString(this.roomId); + } + return stream; + } + + public setDst(dst: string) { + this.dst = dst; + } + + public setSharedPublic(sharedPublic: string) { + this.sharedPublic = sharedPublic; + } + + public setSignalType(signalType: SignalType) { + this.signalType = signalType; + } + + public getDst(): string { + return this.dst; + } + + public getSharedPublic(): string { + return this.sharedPublic; + } + + public getSignalType(): SignalType { + return this.signalType; + } + + public getSrc(): string { + return this.src; + } + + public setSrc(src: string) { + this.src = src; + } + + public getRoomId(): string { + return this.roomId; + } + + public setRoomId(roomId: string) { + this.roomId = roomId; + } + +} \ No newline at end of file