diff --git a/.changeset/poor-months-develop.md b/.changeset/poor-months-develop.md new file mode 100644 index 000000000..d0ca24542 --- /dev/null +++ b/.changeset/poor-months-develop.md @@ -0,0 +1,6 @@ +--- +'@signalwire/webrtc': minor +'@signalwire/js': minor +--- + +CF SDK - Implicit call reattach diff --git a/internal/e2e-js/playwright.config.ts b/internal/e2e-js/playwright.config.ts index 9e088fd22..6dd9b999f 100644 --- a/internal/e2e-js/playwright.config.ts +++ b/internal/e2e-js/playwright.config.ts @@ -37,7 +37,10 @@ const reattachTests = [ ] const callfabricTests = [ 'address.spec.ts', + 'buildVideoElement.spec.ts', 'conversation.spec.ts', + 'reattachWithRoom.spec.ts', + 'reattachWithSWML.spec.ts', 'relayApp.spec.ts', 'swml.spec.ts', 'videoRoom.spec.ts', diff --git a/internal/e2e-js/tests/callfabric/reattach.spec.ts b/internal/e2e-js/tests/callfabric/reattach.spec.ts deleted file mode 100644 index d4fef1ef3..000000000 --- a/internal/e2e-js/tests/callfabric/reattach.spec.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { uuid } from '@signalwire/core' -import { test, expect } from '../../fixtures' -import { - SERVER_URL, - createCFClient, - expectMCUVisible -} from '../../utils' - -test.describe('Reattach Tests', () => { - test('WebRTC to Room', async ({ - createCustomPage, - resource, - }) => { - - const page = await createCustomPage({ name: '[page]' }) - await page.goto(SERVER_URL) - - const roomName = `e2e-video-room_${uuid()}` - await resource.createVideoRoomResource(roomName) - - await createCFClient(page) - - // Dial an address and join a video room - let roomSession = await page.evaluate( - async ({ roomName }) => { - return new Promise(async (resolve, _reject) => { - // @ts-expect-error - const client = window._client - - const call = await client.dial({ - to: `/public/${roomName}`, - rootElement: document.getElementById('rootElement'), - }) - - call.on('call.joined', resolve) - - // @ts-expect-error - window._roomObj = call - - await call.start() - }) - }, - { roomName } - ) - - expect(roomSession.room_session).toBeDefined() - const currentCallId = roomSession.call_id - - await expectMCUVisible(page) - - await page.reload({ waitUntil: 'domcontentloaded'}) - await createCFClient(page) - - // Reattach to an address to join the same call session - roomSession = await page.evaluate( - async ({ roomName }) => { - return new Promise(async (resolve, _reject) => { - // @ts-expect-error - const client = window._client - - const call = await client.reattach({ - to: `/public/${roomName}`, - rootElement: document.getElementById('rootElement'), - }) - - call.on('call.joined', resolve) - - // @ts-expect-error - window._roomObj = call - await call.start() - }) - }, - { roomName } - ) - - expect(roomSession.call_id).toEqual(currentCallId) - // TODO the server is not sending a layout state on reattach - // await expectMCUVisible(page) - }) - - // TODO uncomment after fixed in the backend - // test('WebRTC to SWML to Room', async ({ - // createCustomPage, - // resource, - // }) => { - - // const page = await createCustomPage({ name: '[page]' }) - // await page.goto(SERVER_URL) - - // const roomName = `e2e-video-room_${uuid()}` - // await resource.createVideoRoomResource(roomName) - // const resourceName = `e2e-swml-app_${uuid()}` - // await resource.createSWMLAppResource({ - // name: resourceName, - // contents: { - // sections: { - // main: [ - // 'answer', - // { - // play: { - // volume: 10, - // urls: [ - // 'silence:1.0', - // 'say:Hello, connecting to a fabric resource that is a room', - // ], - // }, - // connect: { - // to: `/public/${roomName}`, - // answer_on_bridge: true - // } - // }, - // ], - // }, - // }, - // }) - - // await createCFClient(page) - - // // Dial an address and join a video room - // let roomSession = await page.evaluate( - // async ({ resourceName }) => { - // return new Promise(async (resolve, _reject) => { - // // @ts-expect-error - // const client = window._client - // let callJoinedCount = 0 - - // const call = await client.dial({ - // to: `/private/${resourceName}`, - // rootElement: document.getElementById('rootElement'), - // }) - - // call.on('call.joined', (event: any) => { - // callJoinedCount++ - // if(callJoinedCount >= 2) { - // resolve(event) - // } - // }) - - // // @ts-expect-error - // window._roomObj = call - - // await call.start() - // }) - // }, - // { resourceName } - // ) - - // expect(roomSession.room_session).toBeDefined() - // const currentCallId = roomSession.call_id - - // await expectMCUVisible(page) - - // await page.reload({ waitUntil: 'domcontentloaded'}) - // await createCFClient(page) - - // // FIXME Server is not accepting the invite - // // Reattach to an address to join the same call session - // roomSession = await page.evaluate( - // async ({ resourceName }) => { - // return new Promise(async (resolve, _reject) => { - // // @ts-expect-error - // const client = window._client - - // const call = await client.reattach({ - // to: `/private/${resourceName}`, - // rootElement: document.getElementById('rootElement'), - // }) - - // call.on('call.joined', resolve) - - // // @ts-expect-error - // window._roomObj = call - // await call.start() - // }) - // }, - // { resourceName } - // ) - - // expect(roomSession.call_id).toEqual(currentCallId) - // // TODO the server is not sending a layout state on reattach - // // await expectMCUVisible(page) - // }) -}) \ No newline at end of file diff --git a/internal/e2e-js/tests/callfabric/reattachWithRoom.spec.ts b/internal/e2e-js/tests/callfabric/reattachWithRoom.spec.ts new file mode 100644 index 000000000..2599b7fd9 --- /dev/null +++ b/internal/e2e-js/tests/callfabric/reattachWithRoom.spec.ts @@ -0,0 +1,333 @@ +import { uuid } from '@signalwire/core' +import { test, expect } from '../../fixtures' +import { + SERVER_URL, + createCFClient, + expectCallJoined, + expectMCUVisible, +} from '../../utils' + +test.describe('CallFabric Reattach with Room', () => { + test('should reattach implicitly', async ({ createCustomPage, resource }) => { + const page = await createCustomPage({ name: '[page]' }) + await page.goto(SERVER_URL) + + const roomName = `e2e-reattach_${uuid()}` + await resource.createVideoRoomResource(roomName) + + await createCFClient(page) + + // Dial an address and join a video room + const roomSession = await expectCallJoined(page, { + to: `/public/${roomName}`, + }) + + expect(roomSession.room_session).toBeDefined() + + await expectMCUVisible(page) + + // --------------- Reattaching --------------- + await page.reload() + + await createCFClient(page) + + console.time('reattach-time') + // Dial the same address and join a video room + const roomSessionReattached = await expectCallJoined(page, { + to: `/public/${roomName}`, + }) + console.timeEnd('reattach-time') + + expect(roomSessionReattached.room_session).toBeDefined() + + await expectMCUVisible(page) + + expect(roomSession.call_id).toBe(roomSessionReattached.call_id) + expect(roomSession.member_id).toBe(roomSessionReattached.member_id) + }) + + test('should reattach implicitly multiple times', async ({ + createCustomPage, + resource, + }) => { + const page = await createCustomPage({ name: '[page]' }) + await page.goto(SERVER_URL) + + const roomName = `e2e-reattach-multiple_${uuid()}` + await resource.createVideoRoomResource(roomName) + + await createCFClient(page) + + // Dial an address and join a video room + const roomSession1 = await expectCallJoined(page, { + to: `/public/${roomName}`, + }) + + expect(roomSession1.room_session).toBeDefined() + + await expectMCUVisible(page) + + // --------------- Reattaching --------------- + await page.reload() + + await createCFClient(page) + + console.time('reattach-time') + // Dial the same address and join a video room again + const roomSession2 = await expectCallJoined(page, { + to: `/public/${roomName}`, + }) + console.timeEnd('reattach-time') + + expect(roomSession2.room_session).toBeDefined() + + await expectMCUVisible(page) + + expect(roomSession1.call_id).toBe(roomSession2.call_id) + expect(roomSession1.member_id).toBe(roomSession2.member_id) + + // --------------- Reattaching --------------- + await page.reload() + + await createCFClient(page) + + console.time('reattach-time') + // Dial the same address and join a video room again + const roomSession3 = await expectCallJoined(page, { + to: `/public/${roomName}`, + }) + console.timeEnd('reattach-time') + + expect(roomSession3.room_session).toBeDefined() + + await expectMCUVisible(page) + + expect(roomSession1.call_id).toBe(roomSession2.call_id) + expect(roomSession1.member_id).toBe(roomSession2.member_id) + }) + + // FIXME: If we dial the same address quickly, the server throws error with `INVALID_CALL_REFERENCE` + test.skip('should not reattach if allowReattach is false', async ({ + createCustomPage, + resource, + }) => { + const page = await createCustomPage({ name: '[page]' }) + await page.goto(SERVER_URL) + + const roomName = `e2e-reattach-fail_${uuid()}` + await resource.createVideoRoomResource(roomName) + + await createCFClient(page) + + // Dial an address and join a video room + const roomSession = await expectCallJoined(page, { + to: `/public/${roomName}`, + }) + + expect(roomSession.room_session).toBeDefined() + + await expectMCUVisible(page) + + // --------------- Reattaching --------------- + await page.reload() + + await createCFClient(page) + + console.time('reattach-time') + // Dial the same address and join a video room + const roomSessionReattached = await expectCallJoined(page, { + to: `/public/${roomName}`, + allowReattach: false, + }) + console.timeEnd('reattach-time') + + expect(roomSessionReattached.room_session).toBeDefined() + + await expectMCUVisible(page) + + expect(roomSession.call_id).not.toBe(roomSessionReattached.call_id) + expect(roomSession.member_id).not.toBe(roomSessionReattached.member_id) + }) + + test('should fail reattach with bad auth', async ({ + createCustomPage, + resource, + }) => { + const page = await createCustomPage({ name: '[page]' }) + await page.goto(SERVER_URL) + + const roomName = `e2e-reattach-bad-auth_${uuid()}` + await resource.createVideoRoomResource(roomName) + + await createCFClient(page) + + // Dial an address and join a video room + const roomSession = await expectCallJoined(page, { + to: `/public/${roomName}`, + }) + expect(roomSession.room_session).toBeDefined() + + await expectMCUVisible(page) + + // --------------- Reattaching --------------- + await page.reload() + + await createCFClient(page) + + console.time('reattach-time') + // Dial the same address with a bogus authorization_state + const roomSessionReattached = await page.evaluate( + async ({ roomName }) => { + // Inject wrong values for authorization state + const key = 'as-SAT' + const state = btoa('just wrong') + window.sessionStorage.setItem(key, state) + console.log( + `Injected authorization state for ${key} with value ${state}` + ) + + // @ts-expect-error + const client = window._client + const call = await client.dial({ + to: `/public/${roomName}`, + rootElement: document.getElementById('rootElement'), + }) + // @ts-expect-error + window._roomObj = call + + // Now try to reattach, which should not succeed + return call.start().catch((error: any) => error) + }, + { roomName } + ) + console.timeEnd('reattach-time') + + const { code, message } = roomSessionReattached + + expect([-32002, '27']).toContain(code) + expect([ + 'CALL ERROR', + 'DESTINATION_OUT_OF_ORDER', + 'Cannot reattach this call with this member ID', + ]).toContain(message) + }) + + test('should fail reattach with wrong call ID', async ({ + createCustomPage, + resource, + }) => { + const page = await createCustomPage({ name: '[page]' }) + await page.goto(SERVER_URL) + + const roomName = `e2e-reattach-wrong-call-id_${uuid()}` + await resource.createVideoRoomResource(roomName) + + await createCFClient(page) + + // Dial an address and join a video room + const roomSession = await expectCallJoined(page, { + to: `/public/${roomName}`, + }) + expect(roomSession.room_session).toBeDefined() + + await expectMCUVisible(page) + + // --------------- Reattaching --------------- + await page.reload() + + await createCFClient(page) + + console.time('reattach-time') + // Dial the same address with a wrong call id + const roomSessionReattached = await page.evaluate( + async ({ roomName }) => { + const destination = `/public/${roomName}` + + // Inject wrong values for authorization state + const key = `ci-${destination}` + const mockId = 'wrong-id' + window.sessionStorage.setItem(key, mockId) + console.log(`Injected callId for ${key} with value ${mockId}`) + + // @ts-expect-error + const client = window._client + const call = await client.dial({ + to: destination, + rootElement: document.getElementById('rootElement'), + }) + // @ts-expect-error + window._roomObj = call + + // Now try to reattach, which should not succeed + return call.start().catch((error: any) => error) + }, + { roomName } + ) + console.timeEnd('reattach-time') + + const { code, message } = roomSessionReattached + expect([-32002, '81']).toContain(code) + expect('callID error').toBe(message) + }) + + test('should fail reattach with wrong protocol', async ({ + createCustomPage, + resource, + }) => { + const page = await createCustomPage({ name: '[page]' }) + await page.goto(SERVER_URL) + + const roomName = `e2e-reattach-wrong-protocol_${uuid()}` + await resource.createVideoRoomResource(roomName) + + await createCFClient(page) + + // Dial an address and join a video room + const roomSession = await expectCallJoined(page, { + to: `/public/${roomName}`, + }) + expect(roomSession.room_session).toBeDefined() + + await expectMCUVisible(page) + + // --------------- Reattaching --------------- + await page.reload() + + await createCFClient(page) + + console.time('reattach-time') + // Dial the same address with a bogus authorization_state + const roomSessionReattached = await page.evaluate( + async ({ roomName }) => { + // Inject wrong values for authorization state + const key = 'pt-SAT' + const state = btoa('wrong protocol') + window.sessionStorage.setItem(key, state) + console.log(`Injected protocol for ${key} with value ${state}`) + + // @ts-expect-error + const client = window._client + const call = await client.dial({ + to: `/public/${roomName}`, + rootElement: document.getElementById('rootElement'), + }) + // @ts-expect-error + window._roomObj = call + + // Now try to reattach, which should not succeed + return call.start().catch((error: any) => error) + }, + { roomName } + ) + console.timeEnd('reattach-time') + + const { code, message } = roomSessionReattached + + expect(-32002).toBe(code) + expect([ + 'CALL ERROR', + 'DESTINATION_OUT_OF_ORDER', + 'Cannot reattach this call with this member ID', + ]).toContain(message) + }) +}) diff --git a/internal/e2e-js/tests/callfabric/reattachWithSWML.spec.ts b/internal/e2e-js/tests/callfabric/reattachWithSWML.spec.ts new file mode 100644 index 000000000..84904d142 --- /dev/null +++ b/internal/e2e-js/tests/callfabric/reattachWithSWML.spec.ts @@ -0,0 +1,136 @@ +import { uuid } from '@signalwire/core' +import { test, expect } from '../../fixtures' +import { + SERVER_URL, + createCFClient, + expectCallJoined, + expectMCUVisible, +} from '../../utils' + +test.describe('CallFabric Reattach with SWML', () => { + test('should reattach with SWML TTS', async ({ + createCustomPage, + resource, + }) => { + const page = await createCustomPage({ name: '[page]' }) + await page.goto(SERVER_URL) + + const swmlTTS = { + sections: { + main: [ + 'answer', + { + play: { + volume: 10, + urls: [ + 'say:Hi', + 'say:Welcome to SignalWire', + "say:Thank you for calling us. All our lines are currently busy, but your call is important to us. Please hang up, and we'll return your call as soon as our representative is available.", + "say:Thank you for calling us. All our lines are currently busy, but your call is important to us. Please hang up, and we'll return your call as soon as our representative is available.", + ], + }, + }, + ], + }, + } + + const resourceName = `e2e-reattach-swml-app_${uuid()}` + await resource.createSWMLAppResource({ + name: resourceName, + contents: swmlTTS, + }) + + await createCFClient(page) + + // Dial an address and join a video room + const roomSession = await expectCallJoined(page, { + to: `/private/${resourceName}`, + }) + + expect(roomSession.room_session).toBeDefined() + + // --------------- Reattaching --------------- + await page.reload() + + await createCFClient(page) + + console.time('reattach-time') + // Dial the same address and join a video room + const roomSessionReattached = await expectCallJoined(page, { + to: `/private/${resourceName}`, + }) + console.timeEnd('reattach-time') + + expect(roomSessionReattached.room_session).toBeDefined() + + expect(roomSession.call_id).toBe(roomSessionReattached.call_id) + expect(roomSession.member_id).toBe(roomSessionReattached.member_id) + }) + + test('should reattach with SWML to Room', async ({ + createCustomPage, + resource, + }) => { + const page = await createCustomPage({ name: '[page]' }) + await page.goto(SERVER_URL) + + const roomName = `e2e-reattach-swml-room_${uuid()}` + await resource.createVideoRoomResource(roomName) + + const swmlToRoom = { + sections: { + main: [ + 'answer', + { + play: { + volume: 10, + urls: [ + 'say:Hello, connecting to a fabric resource that is a room', + ], + }, + connect: { + to: `/public/${roomName}`, + answer_on_bridge: true, + }, + }, + ], + }, + } + + const resourceName = `e2e-reattach-swml-app_${uuid()}` + await resource.createSWMLAppResource({ + name: resourceName, + contents: swmlToRoom, + }) + + await createCFClient(page) + + // Dial an address and join a video room + const roomSession = await expectCallJoined(page, { + to: `/private/${resourceName}`, + }) + + expect(roomSession.room_session).toBeDefined() + + await expectMCUVisible(page) + + // --------------- Reattaching --------------- + await page.reload() + + await createCFClient(page) + + console.time('reattach-time') + // Dial the same address and join a video room + const roomSessionReattached = await expectCallJoined(page, { + to: `/private/${resourceName}`, + }) + console.timeEnd('reattach-time') + + expect(roomSessionReattached.room_session).toBeDefined() + + await expectMCUVisible(page) + + expect(roomSession.call_id).toBe(roomSessionReattached.call_id) + expect(roomSession.member_id).toBe(roomSessionReattached.member_id) + }) +}) diff --git a/internal/e2e-js/utils.ts b/internal/e2e-js/utils.ts index 23b6db442..4aa359ef8 100644 --- a/internal/e2e-js/utils.ts +++ b/internal/e2e-js/utils.ts @@ -1,4 +1,4 @@ -import type { Video } from '@signalwire/js' +import type { DialParams, Video } from '@signalwire/js' import type { MediaEvent } from '@signalwire/webrtc' import { createServer } from 'vite' import path from 'path' @@ -762,8 +762,8 @@ export const createCallWithCompatibilityApi = async ( if (Number.isInteger(Number(response.status)) && response.status !== null) { if (response.status !== 201) { - const responseBody = await response.json(); - const formattedBody = JSON.stringify(responseBody, null, 2); + const responseBody = await response.json() + const formattedBody = JSON.stringify(responseBody, null, 2) console.log( 'ERROR - response from REST API: ', @@ -999,19 +999,21 @@ export const expectInjectRelayHost = async (page: Page, host: string) => { ) } -export const expectInjectIceTransportPolicy = async (page: Page, iceTransportPolicy: string) => { +export const expectInjectIceTransportPolicy = async ( + page: Page, + iceTransportPolicy: string +) => { await page.evaluate( async (params) => { // @ts-expect-error window.__iceTransportPolicy = params.iceTransportPolicy }, { - iceTransportPolicy + iceTransportPolicy, } ) } - export const expectRelayConnected = async ( page: Page, envRelayProject: string, @@ -1086,12 +1088,33 @@ export const expectCFFinalEvents = ( roomObj.on('destroy', () => resolve(true)) }) - return callLeft; + return callLeft }) return Promise.all([finalEvents, ...extraEvents]) } +export const expectCallJoined = (page: Page, options: DialParams) => { + // @ts-expect-error + return page.evaluate((options) => { + return new Promise(async (resolve, reject) => { + // @ts-expect-error + const client = window._client + + const call = await client.dial({ + rootElement: document.getElementById('rootElement'), + ...options, + }) + + call.on('call.joined', resolve) + + // @ts-expect-error + window._roomObj = call + + await call.start().catch(reject) + }) + }, options) +} export interface Resource { id: string diff --git a/packages/js/src/fabric/CallFabricRoomSession.ts b/packages/js/src/fabric/CallFabricRoomSession.ts index 73fcfb7e5..0af264408 100644 --- a/packages/js/src/fabric/CallFabricRoomSession.ts +++ b/packages/js/src/fabric/CallFabricRoomSession.ts @@ -7,9 +7,7 @@ import { VideoMemberEntity, Rooms, VideoLayoutChangedEventParams, - VideoRoomSubscribedEventParams, RoomSessionMember, - getLogger, } from '@signalwire/core' import { BaseRoomSession, @@ -22,8 +20,6 @@ import { MemberCommandWithValueParams, } from '../video' import { BaseConnection } from '@signalwire/webrtc' -import { getStorage } from '../utils/storage' -import { PREVIOUS_CALLID_STORAGE_KEY } from './utils/constants' interface ExecuteActionParams { method: JSONRPCMethod @@ -136,40 +132,8 @@ export class CallFabricRoomSessionConnection extends RoomSessionConnection { }) } - public async start() { - return new Promise(async (resolve, reject) => { - try { - this.once( - 'room.subscribed', - ({ call_id }: VideoRoomSubscribedEventParams) => { - getStorage()?.setItem(PREVIOUS_CALLID_STORAGE_KEY, call_id) - resolve() - } - ) - - this.once('destroy', () => { - getStorage()?.removeItem(PREVIOUS_CALLID_STORAGE_KEY) - }) - - await this.join() - } catch (error) { - this.logger.error('WSClient call start', error) - reject(error) - } - }) - } - - override async join() { - if (this.options.attach) { - this.options.prevCallId = - getStorage()?.getItem(PREVIOUS_CALLID_STORAGE_KEY) ?? undefined - } - getLogger().debug( - `Tying to reattach to previuos call? ${!!this.options - .prevCallId} - prevCallId: ${this.options.prevCallId}` - ) - - return super.join() + public start() { + return this.join() } /** @internal */ diff --git a/packages/js/src/fabric/SignalWire.ts b/packages/js/src/fabric/SignalWire.ts index 68283d07f..ce8d7ce21 100644 --- a/packages/js/src/fabric/SignalWire.ts +++ b/packages/js/src/fabric/SignalWire.ts @@ -23,7 +23,6 @@ export const SignalWire = ( online: wsClient.online.bind(wsClient), offline: wsClient.offline.bind(wsClient), dial: wsClient.dial.bind(wsClient), - reattach: wsClient.reattach.bind(wsClient), handlePushNotification: wsClient.handlePushNotification.bind(wsClient), updateToken: wsClient.updateToken.bind(wsClient), address: { diff --git a/packages/js/src/fabric/WSClient.ts b/packages/js/src/fabric/WSClient.ts index 3244c4bed..476ea07d4 100644 --- a/packages/js/src/fabric/WSClient.ts +++ b/packages/js/src/fabric/WSClient.ts @@ -1,4 +1,9 @@ -import { getLogger, VertoSubscribe, VertoBye } from '@signalwire/core' +import { + getLogger, + VertoSubscribe, + VertoBye, + VideoRoomSubscribedEventParams, +} from '@signalwire/core' import { wsClientWorker } from './workers' import { CallParams, @@ -13,14 +18,8 @@ import { IncomingCallManager } from './IncomingCallManager' import { CallFabricRoomSession } from './CallFabricRoomSession' import { createClient } from './createClient' import { Client } from './Client' - -type BuildRoomParams = Omit & { - attach: boolean - callID?: string - nodeId?: string - sdp?: string - to?: string -} +import { getStorage } from '../utils/storage' +import { UNSAFE_PROP_ACCESS } from '../RoomSession' export class WSClient { private wsClient: Client @@ -61,40 +60,68 @@ export class WSClient { } async dial(params: DialParams) { - return this.connectAndbuildRoomSession({ ...params, attach: false }) - } - - async reattach(params: DialParams) { - return this.connectAndbuildRoomSession({ ...params, attach: true }) - } - - private async connectAndbuildRoomSession(params: BuildRoomParams) { return new Promise(async (resolve, reject) => { try { await this.connect() - const call = this.buildRoomSession(params) + const call = this.buildOutboundCall(params) resolve(call) } catch (error) { - getLogger().error('WSClient', error) + getLogger().error('Unable to connect and create a call', error) reject(error) } }) } - private buildRoomSession(params: BuildRoomParams) { - const { to, callID, nodeId, sdp } = params - + private buildOutboundCall(params: DialParams) { let video = params.video ?? true let negotiateVideo = true - if (to) { - const channelRegex = /\?channel\=(?(audio|video))/ - const result = channelRegex.exec(to) + const channelRegex = /\?channel\=(?(audio|video))/ + const result = channelRegex.exec(params.to) - if (result && result.groups?.channel === 'audio') { - video = false - negotiateVideo = false - } + if (result && result.groups?.channel === 'audio') { + video = false + negotiateVideo = false + } + + const allowReattach = params.allowReattach !== false + const callIdKey = `ci-${params.to}` + const reattachManager = { + joined: ({ call_id }: VideoRoomSubscribedEventParams) => { + if (allowReattach && callIdKey) { + getStorage()?.setItem(callIdKey, call_id) + } + }, + init: () => { + if (allowReattach) { + call.on('room.subscribed', reattachManager.joined) + } + call.options.prevCallId = reattachManager.getPrevCallId() + }, + destroy: () => { + if (!allowReattach) { + return + } + call.off('room.subscribed', reattachManager.joined) + if (callIdKey) { + getStorage()?.removeItem(callIdKey) + } + }, + getPrevCallId: () => { + if (!allowReattach || !callIdKey) { + return + } + // The SDK only reattach if the address matches with the last used address + for (let i = 0; i < sessionStorage.length; i++) { + let key = sessionStorage.key(i) + + // If the key starts with "ci-" and is not the current key, remove it + if (key?.startsWith('ci-') && key !== callIdKey) { + sessionStorage.removeItem(key) + } + } + return getStorage()?.getItem(callIdKey) ?? undefined + }, } const call = this.wsClient.makeCallFabricObject({ @@ -108,17 +135,19 @@ export class WSClient { stopMicrophoneWhileMuted: true, destinationNumber: params.to, watchMediaPackets: false, - remoteSdp: sdp, - prevCallId: callID, - nodeId, disableUdpIceServers: params.disableUdpIceServers || false, - attach: params.attach, userVariables: params.userVariables || this.options.userVariables, + prevCallId: reattachManager.getPrevCallId(), + attach: allowReattach && !!reattachManager.getPrevCallId()?.length, }) // WebRTC connection left the room. call.once('destroy', () => { this.logger.debug('RTC Connection Destroyed') + call.emit('room.left', { reason: call.leaveReason }) + + // Remove callId to reattach + reattachManager.destroy() call.destroy() }) @@ -126,9 +155,47 @@ export class WSClient { this.logger.debug('Session Disconnected') }) - // @ts-expect-error - call.attachPreConnectWorkers() - return call + const start = () => { + return new Promise(async (resolve, reject) => { + try { + // @ts-expect-error + call.attachPreConnectWorkers() + + call.once('room.subscribed', () => resolve()) + + // Hijack previous callId if present + reattachManager.init() + + await call.start() + } catch (error) { + getLogger().error('Failed to start the call', error) + reject(error) + } + }) + } + + const interceptors = { start } + + return new Proxy(call, { + get( + target: CallFabricRoomSession, + prop: keyof CallFabricRoomSession, + receiver: any + ) { + if (prop in interceptors) { + // @ts-expect-error + return interceptors[prop] + } + + if (!target.active && UNSAFE_PROP_ACCESS.includes(prop)) { + throw new Error( + `Tried to access the property/method "${prop}" before the call was started. Please call call.start() first.` + ) + } + + return Reflect.get(target, prop, receiver) + }, + }) } handlePushNotification(payload: PushNotificationPayload) { @@ -231,16 +298,37 @@ export class WSClient { } private buildInboundCall(payload: IncomingInvite, params: CallParams) { - getLogger().debug('Build new call to answer') - const { callID, nodeId, sdp } = payload - return this.buildRoomSession({ - ...params, - attach: false, - callID, + + const call = this.wsClient.makeCallFabricObject({ + audio: params.audio ?? true, + video: params.audio ?? true, + negotiateAudio: params.negotiateAudio ?? true, + negotiateVideo: params.negotiateVideo ?? true, + rootElement: params.rootElement || this.options.rootElement, + applyLocalVideoOverlay: true, + stopCameraWhileMuted: true, + stopMicrophoneWhileMuted: true, + watchMediaPackets: false, + disableUdpIceServers: params.disableUdpIceServers || false, + userVariables: params.userVariables || this.options.userVariables, + prevCallId: callID, nodeId, - sdp, + remoteSdp: sdp, + }) + + // WebRTC connection left the room. + call.once('destroy', () => { + this.logger.debug('RTC Connection Destroyed') + call.emit('room.left', { reason: call.leaveReason }) + call.destroy() + }) + + this.wsClient.once('session.disconnected', () => { + this.logger.debug('Session Disconnected') }) + + return call } /** diff --git a/packages/js/src/fabric/buildCall.ts b/packages/js/src/fabric/buildCall.ts deleted file mode 100644 index 3cd2637db..000000000 --- a/packages/js/src/fabric/buildCall.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { MakeRoomOptions } from '../Client' -import { RoomSession } from '../video' - -type Strategy = 'room' -type StrategyParams = RoomStrategyParams -type RoomStrategyParams = { - token: string -} -interface FabricCallResponse { - strategy: Strategy - params: StrategyParams - userParams: MakeRoomOptions -} - -export const buildCall = ({ - strategy, - params, - userParams, -}: FabricCallResponse) => { - let obj: RoomSession - let start: (...args: any[]) => void - switch (strategy) { - case 'room': - obj = new RoomSession({ - token: params.token, - debug: { - logWsTraffic: true, - }, - logLevel: 'debug', - watchMediaPackets: false, - ...userParams, - }) - start = (joinParams: any) => { - return new Promise((resolve, reject) => { - obj.once('room.joined', (params) => resolve(params)) - // @ts-ignore - obj.emitter.once('verto.display', (params) => resolve(params)) - return obj.join(joinParams).catch((error) => reject(error)) - }) - } - break - // case 'voice': - // case 'script': - // case 'whatever': - default: - throw new Error(`Unknown strategy: '${strategy}'`) - } - - const interceptors = { - start, - } - - return new Proxy(obj, { - get(target: typeof obj, prop: keyof typeof obj, receiver: any) { - if (prop in interceptors) { - // @ts-expect-error - return interceptors[prop] - } - - return Reflect.get(target, prop, receiver) - }, - }) -} diff --git a/packages/js/src/fabric/types.ts b/packages/js/src/fabric/types.ts index e3e2c5e1c..f47a2f1a6 100644 --- a/packages/js/src/fabric/types.ts +++ b/packages/js/src/fabric/types.ts @@ -16,7 +16,6 @@ export interface SignalWireContract { online: WSClient['online'] offline: WSClient['offline'] dial: WSClient['dial'] - reattach: WSClient['reattach'] handlePushNotification: WSClient['handlePushNotification'] updateToken: WSClient['updateToken'] address: { @@ -82,6 +81,7 @@ export interface CallParams { export interface DialParams extends CallParams { to: string nodeId?: string + allowReattach?: boolean } export type CFUserOptions = Omit diff --git a/packages/js/src/fabric/utils/constants.ts b/packages/js/src/fabric/utils/constants.ts deleted file mode 100644 index 07be858d2..000000000 --- a/packages/js/src/fabric/utils/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const PREVIOUS_CALLID_STORAGE_KEY = 'ci-SAT'; \ No newline at end of file diff --git a/packages/webrtc/src/BaseConnection.ts b/packages/webrtc/src/BaseConnection.ts index 3e316f165..bdfa5cbb7 100644 --- a/packages/webrtc/src/BaseConnection.ts +++ b/packages/webrtc/src/BaseConnection.ts @@ -203,7 +203,7 @@ export class BaseConnection dialogParams(rtcPeerId: string) { const { destinationNumber, - attach, + attach, callerName, callerNumber, remoteCallerName,