From 8d2f93e850f55d4793d422d372a51cca7fae32f0 Mon Sep 17 00:00:00 2001 From: Haythem Farhat Date: Thu, 4 Apr 2024 23:42:22 +0100 Subject: [PATCH] feat: add a faster work-around for ice gathering never completing - Added calls to measure time between initiating a call and actually sending the invite - Added utility to created deferred promises and debouncing functions - Refactor Peer.ts, invite and answer calls to wait for ice gathering --- packages/js/src/Modules/Verto/index.ts | 3 +- .../src/Modules/Verto/util/constants/index.ts | 2 +- packages/js/src/Modules/Verto/util/helpers.ts | 33 +++++++++++++ .../js/src/Modules/Verto/webrtc/BaseCall.ts | 47 ++++++------------- packages/js/src/Modules/Verto/webrtc/Peer.ts | 19 +++++++- 5 files changed, 67 insertions(+), 37 deletions(-) diff --git a/packages/js/src/Modules/Verto/index.ts b/packages/js/src/Modules/Verto/index.ts index 83371079..c7169528 100644 --- a/packages/js/src/Modules/Verto/index.ts +++ b/packages/js/src/Modules/Verto/index.ts @@ -7,7 +7,7 @@ import { import { IVertoCallOptions } from './webrtc/interfaces'; import { Login } from './messages/Verto'; import Call from './webrtc/Call'; -import { SESSION_ID } from './util/constants'; +import { SESSION_ID, TIME_CALL_INVITE } from './util/constants'; import { sessionStorage } from './util/storage'; import VertoHandler from './webrtc/VertoHandler'; import { isValidOptions } from './util/helpers'; @@ -42,6 +42,7 @@ export default class Verto extends BrowserSession { throw new Error('Verto.newCall() error: destinationNumber is required.'); } + console.time(TIME_CALL_INVITE) const call = new Call(this, options); call.invite(); return call; diff --git a/packages/js/src/Modules/Verto/util/constants/index.ts b/packages/js/src/Modules/Verto/util/constants/index.ts index 66e50b6e..2ca0d0d6 100644 --- a/packages/js/src/Modules/Verto/util/constants/index.ts +++ b/packages/js/src/Modules/Verto/util/constants/index.ts @@ -2,7 +2,7 @@ export const STORAGE_PREFIX = '@telnyx:'; export const ADD = 'add'; export const REMOVE = 'remove'; export const SESSION_ID = 'sessId'; - +export const TIME_CALL_INVITE = 'Time to call invite'; export const PROD_HOST = 'wss://rtc.telnyx.com'; export const DEV_HOST = 'wss://rtcdev.telnyx.com'; export const STUN_SERVER = { urls: 'stun:stun.telnyx.com:3478' }; diff --git a/packages/js/src/Modules/Verto/util/helpers.ts b/packages/js/src/Modules/Verto/util/helpers.ts index 82b48728..3567f2fb 100644 --- a/packages/js/src/Modules/Verto/util/helpers.ts +++ b/packages/js/src/Modules/Verto/util/helpers.ts @@ -148,3 +148,36 @@ export const getGatewayState = (msg: IMessageRPC): GatewayStateType | '' => { return gateWayState; }; + +export type DeferredPromise = { + promise: Promise; + resolve: (value: T | PromiseLike) => void; + reject: (reason?: any) => void; +}; + +type DeferredPromiseOptions = { + debounceTime?: number; +}; + +export function deferredPromise({ + debounceTime, +}: DeferredPromiseOptions): DeferredPromise { + let resolve: (value: T | PromiseLike) => void; + let reject: (reason?: any) => void; + + const promise = new Promise((res, rej) => { + resolve = debounceTime ? debounce(res, debounceTime) : res; + reject = rej; + }); + return { promise, resolve, reject }; +} + +export const debounce = (func: Function, wait: number) => { + let timeout: number; + return (...args: any) => { + clearTimeout(timeout); + timeout = window.setTimeout(() => { + func(...args); + }, wait); + }; +}; \ No newline at end of file diff --git a/packages/js/src/Modules/Verto/webrtc/BaseCall.ts b/packages/js/src/Modules/Verto/webrtc/BaseCall.ts index 4e26edc9..5144a939 100644 --- a/packages/js/src/Modules/Verto/webrtc/BaseCall.ts +++ b/packages/js/src/Modules/Verto/webrtc/BaseCall.ts @@ -4,7 +4,7 @@ import BrowserSession from '../BrowserSession'; import BaseMessage from '../messages/BaseMessage'; import { Invite, Answer, Attach, Bye, Modify, Info } from '../messages/Verto'; import Peer from './Peer'; -import { SwEvent } from '../util/constants'; +import { SwEvent, TIME_CALL_INVITE } from '../util/constants'; import { INotificationEventData } from '../util/interfaces'; import { State, @@ -246,10 +246,12 @@ export default abstract class BaseCall implements IWebRTCCall { return `conference-member.${this.id}`; } - invite() { + async invite() { this.direction = Direction.Outbound; this.peer = new Peer(PeerType.Offer, this.options); - this._registerPeerEvents(); + await this.peer.iceGatheringComplete.promise; + console.log('icegatheringcompleted') + this._onIceSdp(this.peer.instance.localDescription); } /** @@ -261,20 +263,21 @@ export default abstract class BaseCall implements IWebRTCCall { * call.answer() * ``` */ - answer(params: AnswerParams = {}) { + async answer(params: AnswerParams = {}) { this.stopRingtone(); this.direction = Direction.Inbound; - if(params?.customHeaders?.length > 0) { + if (params?.customHeaders?.length > 0) { this.options = { ...this.options, - customHeaders: params.customHeaders + customHeaders: params.customHeaders, }; } this.peer = new Peer(PeerType.Answer, this.options); - this._registerPeerEvents(); + await this.peer.iceGatheringComplete.promise; + this._onIceSdp(this.peer.instance.localDescription); } playRingtone() { @@ -921,11 +924,11 @@ export default abstract class BaseCall implements IWebRTCCall { if (params.telnyx_call_control_id) { this.options.telnyxCallControlId = params.telnyx_call_control_id; } - + if (params.telnyx_session_id) { this.options.telnyxSessionId = params.telnyx_session_id; } - + if (params.telnyx_leg_id) { this.options.telnyxLegId = params.telnyx_leg_id; } @@ -1373,6 +1376,7 @@ export default abstract class BaseCall implements IWebRTCCall { this._iceTimeout = null; this._iceDone = true; const { sdp, type } = data; + if (sdp.indexOf('candidate') === -1) { logger.info('No candidate - retry \n'); this._requestAnotherLocalDescription(); @@ -1393,6 +1397,7 @@ export default abstract class BaseCall implements IWebRTCCall { switch (type) { case PeerType.Offer: this.setState(State.Requesting); + console.timeEnd(TIME_CALL_INVITE); msg = new Invite(tmpParams); break; case PeerType.Answer: @@ -1437,30 +1442,6 @@ export default abstract class BaseCall implements IWebRTCCall { } } - private _registerPeerEvents() { - const { instance } = this.peer; - this._iceDone = false; - instance.onicecandidate = (event) => { - if (this._iceDone) { - return; - } - this._onIce(event); - }; - - //@ts-ignore - instance.addEventListener('addstream', (event: MediaStreamEvent) => { - this.options.remoteStream = event.stream; - }); - - instance.addEventListener('track', (event: RTCTrackEvent) => { - this.options.remoteStream = event.streams[0]; - const { remoteElement, remoteStream, screenShare } = this.options; - if (screenShare === false) { - attachMediaStream(remoteElement, remoteStream); - } - }); - } - private _checkConferenceSerno = (serno: number) => { const check = serno < 0 || diff --git a/packages/js/src/Modules/Verto/webrtc/Peer.ts b/packages/js/src/Modules/Verto/webrtc/Peer.ts index 2d7c90a5..caee7cf8 100644 --- a/packages/js/src/Modules/Verto/webrtc/Peer.ts +++ b/packages/js/src/Modules/Verto/webrtc/Peer.ts @@ -16,7 +16,12 @@ import { RTCPeerConnection, streamIsValid, } from '../util/webrtc'; -import { isFunction } from '../util/helpers'; +import { + DeferredPromise, + debounce, + deferredPromise, + isFunction, +} from '../util/helpers'; import { IVertoCallOptions } from './interfaces'; import { trigger } from '../services/Handler'; @@ -25,7 +30,7 @@ import { trigger } from '../services/Handler'; */ export default class Peer { public instance: RTCPeerConnection; - + public iceGatheringComplete: DeferredPromise; public onSdpReadyTwice: Function = null; private _constraints: { @@ -50,6 +55,7 @@ export default class Peer { this.handleNegotiationNeededEvent.bind(this); this.handleTrackEvent = this.handleTrackEvent.bind(this); this.createPeerConnection = this.createPeerConnection.bind(this); + this.iceGatheringComplete = deferredPromise({ debounceTime: 100 }); this._init(); } @@ -130,12 +136,21 @@ export default class Peer { } } + private handleIceCandidate = (event: RTCPeerConnectionIceEvent) => { + console.log(event.candidate); + if (event.candidate && ['relay', 'srflx'].includes(event.candidate.type)) { + // Found enough candidates to establish a connection + // This is a workaround for the issue where iceGatheringState is always 'gathering' + this.iceGatheringComplete.resolve(true); + } + }; private async createPeerConnection() { this.instance = RTCPeerConnection(this._config()); this.instance.onsignalingstatechange = this.handleSignalingStateChangeEvent; this.instance.onnegotiationneeded = this.handleNegotiationNeededEvent; this.instance.ontrack = this.handleTrackEvent; + this.instance.addEventListener('icecandidate', this.handleIceCandidate); //@ts-ignore this.instance.addEventListener('addstream', (event: MediaStreamEvent) => {