Skip to content

WIP: 1:1 Calls local mute fallback #993

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 130 additions & 69 deletions packages/js/src/fabric/WSClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import { Client } from '../Client'
import { RoomSession } from '../RoomSession'
import { createClient } from '../createClient'
import { wsClientWorker, unifiedEventsWatcher } from './workers'
import { InboundCallSource, IncomingCallHandlers, IncomingCallManager, IncomingInvite } from './IncomingCallManager'
import {
InboundCallSource,
IncomingCallHandlers,
IncomingCallManager,
IncomingInvite,
} from './IncomingCallManager'

export interface OnlineParams {
incomingCallHandlers: IncomingCallHandlers
Expand All @@ -28,6 +33,14 @@ interface PushNotification {
decrypted: Record<string, any>
}

interface MakeRoomParams {
to?: string | undefined;
nodeId?: string | undefined;
sdp?: string | undefined;
callID?: string | undefined;
rootElement: HTMLElement | undefined;
}

export interface WSClientOptions extends UserOptions {
/** HTML element in which to display the video stream */
rootElement?: HTMLElement
Expand All @@ -48,8 +61,10 @@ export class WSClient {
unifiedEventing: true,
})
this._incomingCallManager = new IncomingCallManager(
(payload: IncomingInvite,rootElement: HTMLElement | undefined) => this.buildInboundCall(payload, rootElement),
(callId: string, nodeId: string) => this.executeVertoBye(callId, nodeId))
(payload: IncomingInvite, rootElement: HTMLElement | undefined) =>
this.buildInboundCall(payload, rootElement),
(callId: string, nodeId: string) => this.executeVertoBye(callId, nodeId)
)
}

/** @internal */
Expand All @@ -62,7 +77,8 @@ export class WSClient {
this.wsClient.runWorker('wsClientWorker', {
worker: wsClientWorker,
initialState: {
buildInboundCall: (incomingInvite: Omit<IncomingInvite, 'source'>) => this.notifyIncomingInvite('websocket', incomingInvite),
buildInboundCall: (incomingInvite: Omit<IncomingInvite, 'source'>) =>
this.notifyIncomingInvite('websocket', incomingInvite),
},
})
await this.wsClient.connect()
Expand All @@ -82,36 +98,69 @@ export class WSClient {
console.log('WSClient dial with:', params)

await this.connect()
const call = this.wsClient.rooms.makeRoomObject({
// audio,
// video: video === true ? VIDEO_CONSTRAINTS : video,
negotiateAudio: true,
negotiateVideo: true,
// iceServers,
rootElement: params.rootElement ?? this.options.rootElement,
applyLocalVideoOverlay: true,
stopCameraWhileMuted: true,
stopMicrophoneWhileMuted: true,
// speakerId,
destinationNumber: params.to,
watchMediaPackets: false,
// watchMediaPacketsTimeout:,
nodeId: params.nodeId,
eventsWatcher: unifiedEventsWatcher,
disableUdpIceServers: this.options.disableUdpIceServers || false,
})
const callProxy = this._makeRoom(params)

// WebRTC connection left the room.
call.once('destroy', () => {
this.logger.debug('RTC Connection Destroyed')
})
resolve(callProxy)
} catch (error) {
getLogger().error('WSClient dial', error)

this.wsClient.once('session.disconnected', () => {
this.logger.debug('Session Disconnected')
})
reject(error)
}
})
}

// @ts-expect-error
call.attachPreConnectWorkers()
private _makeRoom(params: MakeRoomParams) {
const call = this.wsClient.rooms.makeRoomObject({
negotiateAudio: true,
negotiateVideo: true,
rootElement: params.rootElement ?? this.options.rootElement,
applyLocalVideoOverlay: true,
stopCameraWhileMuted: true,
stopMicrophoneWhileMuted: true,
destinationNumber: params.to,
watchMediaPackets: false,
nodeId: params.nodeId,
remoteSdp: params.sdp,
prevCallId: params.callID,
eventsWatcher: unifiedEventsWatcher,
disableUdpIceServers: this.options.disableUdpIceServers || false,
})

// WebRTC connection left the room.
call.once('destroy', () => {
this.logger.debug('RTC Connection Destroyed')
})

this.wsClient.once('session.disconnected', () => {
this.logger.debug('Session Disconnected')
})

// @ts-expect-error
call.attachPreConnectWorkers()

const notImplementedLocalFallback = async ({
args, memberId, muted, updated, method,
}: {
args: any
memberId: string
muted: boolean
updated: 'audio_muted' | 'video_muted'
method: (params: any) => Promise<void>
}) => {
try {
await method(args)
} catch (e) {
if (e.code === '501' && e.message === 'Not implemented') {
this.logger.warn(
'Received "Not implemented" from the server, handling localy only',
{ args, updated, muted }
)
call.emit(`member.updated.${updated}`, {
member: { id: memberId, [`${updated}`]: muted },
})
}
}
}

// @ts-expect-error
call.start = () => {
Expand All @@ -122,21 +171,53 @@ export class WSClient {
resolve(call)
})

await call.join()
} catch (error) {
getLogger().error('WSClient call start', error)
reject(error)
}
})
await call.join()
} catch (error) {
getLogger().error('WSClient call start', error)
reject(error)
}
})
}

resolve(call)
} catch (error) {
getLogger().error('WSClient dial', error)
const callProxy = new Proxy(call, {
get(target: typeof call, prop: keyof typeof call, receiver: any) {
if (prop == 'audioMute')
return async (params: any) => notImplementedLocalFallback({
args: params,
muted: true,
updated: 'audio_muted',
memberId: call.memberId,
method: (p: any) => call.audioMute(p),
})
if (prop == 'audioUnmute')
return async (params: any) => notImplementedLocalFallback({
args: params,
muted: false,
updated: 'audio_muted',
memberId: call.memberId,
method: (p: any) => call.audioUnmute(p),
})
if (prop == 'videoMute')
return async (params: any) => notImplementedLocalFallback({
args: params,
muted: true,
updated: 'video_muted',
memberId: call.memberId,
method: (p: any) => call.videoMute(p),
})
if (prop == 'videoUnmute')
return async (params: any) => notImplementedLocalFallback({
args: params,
muted: false,
updated: 'video_muted',
memberId: call.memberId,
method: (p: any) => call.videoUnmute(p),
})

reject(error)
}
return Reflect.get(target, prop, receiver)
},
})
return callProxy
}

handlePushNotification(payload: PushNotification) {
Expand Down Expand Up @@ -197,10 +278,13 @@ export class WSClient {
})
}

private notifyIncomingInvite(source: InboundCallSource, buildCallParams: Omit<IncomingInvite, 'source'>) {
private notifyIncomingInvite(
source: InboundCallSource,
buildCallParams: Omit<IncomingInvite, 'source'>
) {
this._incomingCallManager.handleIncomingInvite({
source,
...buildCallParams
...buildCallParams,
})
}

Expand Down Expand Up @@ -261,30 +345,7 @@ export class WSClient {
this.wsClient.rooms.makeRoomObject
)

const call = this.wsClient.rooms.makeRoomObject({
negotiateAudio: true,
negotiateVideo: true,
rootElement: rootElement ?? this.options.rootElement,
applyLocalVideoOverlay: true,
stopCameraWhileMuted: true,
stopMicrophoneWhileMuted: true,
watchMediaPackets: false,
remoteSdp: sdp,
prevCallId: callID,
nodeId,
eventsWatcher: unifiedEventsWatcher,
disableUdpIceServers: this.options.disableUdpIceServers || false,
})

// WebRTC connection left the room.
call.once('destroy', () => {
getLogger().debug('RTC Connection Destroyed')
})

// @ts-expect-error
call.attachPreConnectWorkers()

getLogger().debug('Resolving Call', call)
const call = this._makeRoom({nodeId, rootElement, sdp, callID})

return call
}
Expand All @@ -309,7 +370,7 @@ export class WSClient {
/**
* Mark the client as 'online' to receive calls over WebSocket
*/
online({incomingCallHandlers}: OnlineParams) {
online({ incomingCallHandlers }: OnlineParams) {
this._incomingCallManager.setNotificationHandlers(incomingCallHandlers)
// @ts-expect-error
return this.wsClient.execute({
Expand Down