Skip to content

Commit

Permalink
feat: add a faster work-around for ice gathering never completing (#349)
Browse files Browse the repository at this point in the history
* 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

* feat: add prefetching, and use google stun for faster ice gathering

* chore: remove unused import
  • Loading branch information
farhat-ha authored Apr 5, 2024
1 parent 2aa0db9 commit 47e1863
Show file tree
Hide file tree
Showing 6 changed files with 70 additions and 41 deletions.
7 changes: 3 additions & 4 deletions packages/js/src/Modules/Verto/BrowserSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -612,12 +612,11 @@ export default abstract class BrowserSession extends BaseSession {
}

set iceServers(servers: RTCIceServer[] | boolean) {
const googleStun = { urls: ['stun:stun.l.google.com:19302'] };
if (typeof servers === 'boolean') {
this._iceServers = servers
? [{ urls: ['stun:stun.l.google.com:19302'] }]
: [];
this._iceServers = servers ? [googleStun] : [];
} else {
this._iceServers = servers || [TURN_SERVER, STUN_SERVER];
this._iceServers = servers || [TURN_SERVER, STUN_SERVER, googleStun];
}
}

Expand Down
3 changes: 2 additions & 1 deletion packages/js/src/Modules/Verto/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion packages/js/src/Modules/Verto/util/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' };
Expand Down
33 changes: 33 additions & 0 deletions packages/js/src/Modules/Verto/util/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,36 @@ export const getGatewayState = (msg: IMessageRPC): GatewayStateType | '' => {

return gateWayState;
};

export type DeferredPromise<T> = {
promise: Promise<T>;
resolve: (value: T | PromiseLike<T>) => void;
reject: (reason?: any) => void;
};

type DeferredPromiseOptions = {
debounceTime?: number;
};

export function deferredPromise<T>({
debounceTime,
}: DeferredPromiseOptions): DeferredPromise<T> {
let resolve: (value: T | PromiseLike<T>) => void;
let reject: (reason?: any) => void;

const promise = new Promise<T>((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);
};
};
48 changes: 15 additions & 33 deletions packages/js/src/Modules/Verto/webrtc/BaseCall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -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() {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -1419,6 +1423,8 @@ export default abstract class BaseCall implements IWebRTCCall {
logger.error(`${this.id} - Sending ${type} error:`, error);
this.hangup();
});
console.timeEnd(TIME_CALL_INVITE);

}

private _onIce(event: RTCPeerConnectionIceEvent) {
Expand All @@ -1437,30 +1443,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 ||
Expand Down
18 changes: 16 additions & 2 deletions packages/js/src/Modules/Verto/webrtc/Peer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ import {
RTCPeerConnection,
streamIsValid,
} from '../util/webrtc';
import { isFunction } from '../util/helpers';
import {
DeferredPromise,
deferredPromise,
isFunction,
} from '../util/helpers';
import { IVertoCallOptions } from './interfaces';
import { trigger } from '../services/Handler';

Expand All @@ -25,7 +29,7 @@ import { trigger } from '../services/Handler';
*/
export default class Peer {
public instance: RTCPeerConnection;

public iceGatheringComplete: DeferredPromise<boolean>;
public onSdpReadyTwice: Function = null;

private _constraints: {
Expand All @@ -50,6 +54,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();
}
Expand Down Expand Up @@ -130,12 +135,20 @@ export default class Peer {
}
}

private handleIceCandidate = (event: RTCPeerConnectionIceEvent) => {
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) => {
Expand Down Expand Up @@ -365,6 +378,7 @@ export default class Peer {
const { iceServers = [] } = this.options;

const config: RTCConfiguration = {
iceCandidatePoolSize: 255,
bundlePolicy: 'max-compat',
iceServers,
};
Expand Down

0 comments on commit 47e1863

Please sign in to comment.