import _ from 'lodash'; import { SignalService } from '../../protobuf'; import { CallMessage } from '../messages/outgoing/controlMessage/CallMessage'; import { ed25519Str } from '../onions/onionPath'; import { getMessageQueue } from '../sending'; import { PubKey } from '../types'; import { sleepFor } from './Promise'; const incomingCall = ({ sender }: { sender: string }) => { return { type: 'incomingCall', payload: sender }; }; const endCall = ({ sender }: { sender: string }) => { return { type: 'endCall', payload: sender }; }; const answerCall = ({ sender, sdps }: { sender: string; sdps: Array }) => { return { type: 'answerCall', payload: { sender, sdps, }, }; }; /** * This field stores all the details received by a sender about a call in separate messages. */ const callCache = new Map>(); let peerConnection: RTCPeerConnection | null; const ENABLE_VIDEO = true; const configuration = { configuration: { offerToReceiveAudio: true, offerToReceiveVideo: ENABLE_VIDEO, }, iceServers: [ { urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun1.l.google.com:19302' }, { urls: 'stun:stun2.l.google.com:19302' }, { urls: 'stun:stun3.l.google.com:19302' }, { urls: 'stun:stun4.l.google.com:19302' }, ], }; // tslint:disable-next-line: function-name export async function USER_callRecipient(recipient: string) { window?.log?.info(`starting call with ${ed25519Str(recipient)}..`); if (peerConnection) { window.log.info('closing existing peerconnection'); peerConnection.close(); peerConnection = null; } peerConnection = new RTCPeerConnection(configuration); const mediadevices = await openMediaDevices(); mediadevices.getTracks().map(track => { window.log.info('USER_callRecipient adding track: ', track); peerConnection?.addTrack(track); }); peerConnection.addEventListener('connectionstatechange', _event => { window.log.info('peerConnection?.connectionState:', peerConnection?.connectionState); if (peerConnection?.connectionState === 'connected') { // Peers connected! } }); peerConnection.addEventListener('icecandidate', event => { // window.log.warn('event.candidate', event.candidate); if (event.candidate) { iceCandidates.push(event.candidate); void iceSenderDebouncer(recipient); } }); const offerDescription = await peerConnection.createOffer({ offerToReceiveAudio: true, offerToReceiveVideo: ENABLE_VIDEO, }); if (!offerDescription || !offerDescription.sdp || !offerDescription.sdp.length) { window.log.warn(`failed to createOffer for recipient ${ed25519Str(recipient)}`); return; } await peerConnection.setLocalDescription(offerDescription); const callOfferMessage = new CallMessage({ timestamp: Date.now(), type: SignalService.CallMessage.Type.OFFER, sdps: [offerDescription.sdp], }); window.log.info('sending OFFER MESSAGE'); await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(recipient), callOfferMessage); // FIXME audric dispatch UI update to show the calling UI } const iceCandidates: Array = new Array(); const iceSenderDebouncer = _.debounce(async (recipient: string) => { if (!iceCandidates) { return; } const validCandidates = _.compact( iceCandidates.map(c => { if ( c.sdpMLineIndex !== null && c.sdpMLineIndex !== undefined && c.sdpMid !== null && c.candidate ) { return { sdpMLineIndex: c.sdpMLineIndex, sdpMid: c.sdpMid, candidate: c.candidate, }; } return null; }) ); const callIceCandicates = new CallMessage({ timestamp: Date.now(), type: SignalService.CallMessage.Type.ICE_CANDIDATES, sdpMLineIndexes: validCandidates.map(c => c.sdpMLineIndex), sdpMids: validCandidates.map(c => c.sdpMid), sdps: validCandidates.map(c => c.candidate), }); window.log.info('sending ICE CANDIDATES MESSAGE to ', recipient); await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(recipient), callIceCandicates); }, 2000); const openMediaDevices = async () => { return navigator.mediaDevices.getUserMedia({ // video: video: ENABLE_VIDEO ? { width: 320, height: 240, } : false, audio: true, }); }; const printStatsLoop = async () => { // tslint:disable-next-line: no-constant-condition while (true) { if (peerConnection) { const stats = await peerConnection?.getStats(); stats.forEach(st => { console.warn('stat: ', st); }); } await sleepFor(5000); } }; // tslint:disable-next-line: function-name export async function USER_acceptIncomingCallRequest(fromSender: string) { const msgCacheFromSender = callCache.get(fromSender); if (!msgCacheFromSender) { window?.log?.info( 'incoming call request cannot be accepted as the corresponding message is not found' ); return; } const lastOfferMessage = _.findLast( msgCacheFromSender, m => m.type === SignalService.CallMessage.Type.OFFER ); if (!lastOfferMessage) { window?.log?.info( 'incoming call request cannot be accepted as the corresponding message is not found' ); return; } if (peerConnection) { window.log.info('closing existing peerconnection'); peerConnection.close(); peerConnection = null; } peerConnection = new RTCPeerConnection(configuration); const mediadevices = await openMediaDevices(); mediadevices.getTracks().map(track => { window.log.info('USER_acceptIncomingCallRequest adding track ', track); peerConnection?.addTrack(track); }); peerConnection.addEventListener('icecandidateerror', event => { console.warn('icecandidateerror:', event); }); peerConnection.addEventListener('negotiationneeded', event => { console.warn('negotiationneeded:', event); }); peerConnection.addEventListener('signalingstatechange', event => { console.warn('signalingstatechange:', event); }); peerConnection.addEventListener('ontrack', event => { console.warn('ontrack:', event); }); peerConnection.addEventListener('connectionstatechange', _event => { window.log.info('peerConnection?.connectionState:', peerConnection?.connectionState, _event); if (peerConnection?.connectionState === 'connected') { // Peers connected! } }); const { sdps } = lastOfferMessage; if (!sdps || sdps.length === 0) { window?.log?.info( 'incoming call request cannot be accepted as the corresponding sdps is empty' ); return; } await peerConnection.setRemoteDescription( new RTCSessionDescription({ sdp: sdps[0], type: 'offer' }) ); const answer = await peerConnection.createAnswer({ offerToReceiveAudio: true, offerToReceiveVideo: ENABLE_VIDEO, }); if (!answer?.sdp || answer.sdp.length === 0) { window.log.warn('failed to create answer'); return; } await peerConnection.setLocalDescription(answer); const answerSdp = answer.sdp; const callAnswerMessage = new CallMessage({ timestamp: Date.now(), type: SignalService.CallMessage.Type.ANSWER, sdps: [answerSdp], }); window.log.info('sending ANSWER MESSAGE'); await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(fromSender), callAnswerMessage); window.inboxStore?.dispatch(answerCall({ sender: fromSender, sdps })); } // tslint:disable-next-line: function-name export async function USER_rejectIncomingCallRequest(fromSender: string) { const endCallMessage = new CallMessage({ type: SignalService.CallMessage.Type.END_CALL, timestamp: Date.now(), }); callCache.delete(fromSender); window.inboxStore?.dispatch(endCall({ sender: fromSender })); window.log.info('sending END_CALL MESSAGE'); await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(fromSender), endCallMessage); } export function handleEndCallMessage(sender: string) { callCache.delete(sender); // // FIXME audric trigger UI cleanup window.inboxStore?.dispatch(endCall({ sender })); } export async function handleOfferCallMessage( sender: string, callMessage: SignalService.CallMessage ) { if (!callCache.has(sender)) { callCache.set(sender, new Array()); } callCache.get(sender)?.push(callMessage); window.inboxStore?.dispatch(incomingCall({ sender })); //FIXME audric. thiis should not be auto accepted here await USER_acceptIncomingCallRequest(sender); } export async function handleCallAnsweredMessage( sender: string, callMessage: SignalService.CallMessage ) { if (!callMessage.sdps || callMessage.sdps.length === 0) { window.log.warn('cannot handle answered message without sdps'); return; } if (!callCache.has(sender)) { callCache.set(sender, new Array()); } callCache.get(sender)?.push(callMessage); window.inboxStore?.dispatch(incomingCall({ sender })); const remoteDesc = new RTCSessionDescription({ type: 'answer', sdp: callMessage.sdps[0] }); if (peerConnection) { await peerConnection.setRemoteDescription(remoteDesc); } else { window.log.info('call answered by recipient but we do not have a peerconnection set'); } } export async function handleIceCandidatesMessage( sender: string, callMessage: SignalService.CallMessage ) { if (!callMessage.sdps || callMessage.sdps.length === 0) { window.log.warn('cannot handle iceCandicates message without candidates'); return; } if (!callCache.has(sender)) { callCache.set(sender, new Array()); } callCache.get(sender)?.push(callMessage); window.inboxStore?.dispatch(incomingCall({ sender })); if (peerConnection) { // tslint:disable-next-line: prefer-for-of for (let index = 0; index < callMessage.sdps.length; index++) { const sdp = callMessage.sdps[index]; const sdpMLineIndex = callMessage.sdpMLineIndexes[index]; const sdpMid = callMessage.sdpMids[index]; const candicate = new RTCIceCandidate({ sdpMid, sdpMLineIndex, candidate: sdp }); await peerConnection.addIceCandidate(candicate); } } else { window.log.info('handleIceCandidatesMessage but we do not have a peerconnection set'); } } // tslint:disable-next-line: no-async-without-await export async function handleOtherCallMessage( sender: string, callMessage: SignalService.CallMessage ) { callCache.get(sender)?.push(callMessage); } void printStatsLoop();