From 1566b39df62337d3e4325d9a67d0ed76fc689b33 Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Thu, 10 Apr 2025 12:34:39 +0200 Subject: [PATCH 01/10] feat: ring individual members --- packages/client/src/Call.ts | 14 +++++++--- packages/client/src/gen/coordinator/index.ts | 28 ++++++++++++++++++-- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/packages/client/src/Call.ts b/packages/client/src/Call.ts index 1b69546560..b26640843d 100644 --- a/packages/client/src/Call.ts +++ b/packages/client/src/Call.ts @@ -684,12 +684,16 @@ export class Call { * @param params.ring if set to true, a `call.ring` event will be sent to the call members. * @param params.notify if set to true, a `call.notification` event will be sent to the call members. * @param params.members_limit the total number of members to return as part of the response. + * @param params.video if set to true, the call will be created as a video call. + * @param params.member_ids the list of members to ring. Limited to 100 members per request. */ get = async (params?: { ring?: boolean; notify?: boolean; members_limit?: number; - }) => { + video?: boolean; + member_ids?: string[]; + }): Promise => { await this.setup(); const response = await this.streamClient.get( this.streamClientBasePath, @@ -770,9 +774,13 @@ export class Call { /** * A shortcut for {@link Call.get} with `ring` parameter set to `true`. * Will send a `call.ring` event to the call members. + * + * @param params.member_ids the list of members to ring. Limited to 100 members per request. */ - ring = async (): Promise => { - return await this.get({ ring: true }); + ring = async (params: { + member_ids?: string[]; + }): Promise => { + return await this.get({ ...params, ring: true }); }; /** diff --git a/packages/client/src/gen/coordinator/index.ts b/packages/client/src/gen/coordinator/index.ts index 2dbdf04139..9ab34d0e48 100644 --- a/packages/client/src/gen/coordinator/index.ts +++ b/packages/client/src/gen/coordinator/index.ts @@ -2093,6 +2093,12 @@ export interface CallSessionResponse { * @memberof CallSessionResponse */ anonymous_participant_count: number; + /** + * + * @type {string} + * @memberof CallSessionResponse + */ + created_at: string; /** * * @type {string} @@ -4777,6 +4783,12 @@ export interface OwnUserResponse { * @memberof OwnUserResponse */ teams: Array; + /** + * + * @type {{ [key: string]: string; }} + * @memberof OwnUserResponse + */ + teams_role?: { [key: string]: string }; /** * * @type {string} @@ -6923,7 +6935,7 @@ export interface UserInfoResponse { */ export interface UserRequest { /** - * + * Custom user data * @type {{ [key: string]: any; }} * @memberof UserRequest */ @@ -6935,7 +6947,7 @@ export interface UserRequest { */ id: string; /** - * + * User's profile image URL * @type {string} * @memberof UserRequest */ @@ -7049,6 +7061,12 @@ export interface UserResponse { * @memberof UserResponse */ teams: Array; + /** + * + * @type {{ [key: string]: string; }} + * @memberof UserResponse + */ + teams_role?: { [key: string]: string }; /** * Date/time of the last update * @type {string} @@ -7152,6 +7170,12 @@ export interface UserResponsePrivacyFields { * @memberof UserResponsePrivacyFields */ teams: Array; + /** + * + * @type {{ [key: string]: string; }} + * @memberof UserResponsePrivacyFields + */ + teams_role?: { [key: string]: string }; /** * * @type {string} From 3746df4c81209219e48744e68d4675f4d3b3848e Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Fri, 4 Jul 2025 16:17:23 +0200 Subject: [PATCH 02/10] fix: ensure memberIds are comma separated --- packages/client/src/Call.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/Call.ts b/packages/client/src/Call.ts index 5da14d562b..e0c568aafd 100644 --- a/packages/client/src/Call.ts +++ b/packages/client/src/Call.ts @@ -719,7 +719,7 @@ export class Call { await this.setup(); const response = await this.streamClient.get( this.streamClientBasePath, - params, + { ...params, member_ids: params?.member_ids?.join(',') }, ); this.state.updateFromCallResponse(response.call); From 44f79a977a0e71a57761d1c3b528aef499484ac2 Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Mon, 7 Jul 2025 11:28:14 +0200 Subject: [PATCH 03/10] fix: adjust to the latest schema --- packages/client/src/Call.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/client/src/Call.ts b/packages/client/src/Call.ts index e0c568aafd..744df68bf0 100644 --- a/packages/client/src/Call.ts +++ b/packages/client/src/Call.ts @@ -706,20 +706,20 @@ export class Call { * @param params.ring if set to true, a `call.ring` event will be sent to the call members. * @param params.notify if set to true, a `call.notification` event will be sent to the call members. * @param params.members_limit the total number of members to return as part of the response. - * @param params.video if set to true, the call will be created as a video call. - * @param params.member_ids the list of members to ring. Limited to 100 members per request. + * @param params.video if set to true, in a ringing scenario, mobile SDKs will show "incoming video call", audio only otherwise. + * @param params.target_member_ids the list of members to ring. Limited to 100 members per request. */ get = async (params?: { ring?: boolean; notify?: boolean; members_limit?: number; video?: boolean; - member_ids?: string[]; + target_member_ids?: string[]; }): Promise => { await this.setup(); const response = await this.streamClient.get( this.streamClientBasePath, - { ...params, member_ids: params?.member_ids?.join(',') }, + { ...params, target_member_ids: params?.target_member_ids?.join(',') }, ); this.state.updateFromCallResponse(response.call); @@ -798,7 +798,7 @@ export class Call { * @param params.member_ids the list of members to ring. Limited to 100 members per request. */ ring = async (params: { - member_ids?: string[]; + target_member_ids?: string[]; }): Promise => { return await this.get({ ...params, ring: true }); }; From 6f282b1b87bc548e4105cabf850d8e4d55643812 Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Thu, 10 Jul 2025 12:58:27 +0200 Subject: [PATCH 04/10] fix: call creator isn't always the ringing initiator --- packages/client/src/StreamVideoClient.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/client/src/StreamVideoClient.ts b/packages/client/src/StreamVideoClient.ts index 42196cd099..89351b0c17 100644 --- a/packages/client/src/StreamVideoClient.ts +++ b/packages/client/src/StreamVideoClient.ts @@ -207,13 +207,6 @@ export class StreamVideoClient { this.eventHandlersToUnregister.push( this.on('call.ring', async (event) => { const { call, members } = event; - if (this.state.connectedUser?.id === call.created_by.id) { - this.logger( - 'debug', - 'Received `call.ring` sent by the current user so ignoring the event', - ); - return; - } // if `call.created` was received before `call.ring`. // the client already has the call instance and we just need to update the state const theCall = this.writeableStateStore.findCall(call.type, call.id); @@ -360,6 +353,7 @@ export class StreamVideoClient { * * @param type the type of the call. * @param id the id of the call. + * @param options additional options for the call. */ call = ( type: string, From 6eb5887a26e37385c035994e119a7985334ffc82 Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Thu, 10 Jul 2025 12:58:44 +0200 Subject: [PATCH 05/10] fix: add tests --- .../__tests__/StreamVideoClient.api.test.ts | 24 +-- .../StreamVideoClient.ringing.test.ts | 185 ++++++++++++++++++ 2 files changed, 190 insertions(+), 19 deletions(-) create mode 100644 packages/client/src/__tests__/StreamVideoClient.ringing.test.ts diff --git a/packages/client/src/__tests__/StreamVideoClient.api.test.ts b/packages/client/src/__tests__/StreamVideoClient.api.test.ts index 5ff52c3752..c204f20190 100644 --- a/packages/client/src/__tests__/StreamVideoClient.api.test.ts +++ b/packages/client/src/__tests__/StreamVideoClient.api.test.ts @@ -10,34 +10,20 @@ const secret = process.env.STREAM_SECRET!; const serverClient = new StreamClient(apiKey, secret); -const tokenProvider = (userId: string) => { - return async () => { - return new Promise((resolve) => { - setTimeout(() => { - const token = serverClient.createToken( - userId, - undefined, - Math.round(Date.now() / 1000 - 10), - ); - resolve(token); - }, 100); - }); - }; -}; - describe('StreamVideoClient - coordinator API', () => { let client: StreamVideoClient; - const user = { - id: 'sara', - }; beforeAll(() => { + const user = { id: 'sara' }; client = new StreamVideoClient(apiKey, { // tests run in node, so we have to fake being in browser env browser: true, timeout: 15000, }); - client.connectUser(user, tokenProvider(user.id)); + client.connectUser( + user, + serverClient.generateUserToken({ user_id: user.id }), + ); }); it('query calls', { retry: 3, timeout: 20000 }, async () => { diff --git a/packages/client/src/__tests__/StreamVideoClient.ringing.test.ts b/packages/client/src/__tests__/StreamVideoClient.ringing.test.ts new file mode 100644 index 0000000000..4441812c55 --- /dev/null +++ b/packages/client/src/__tests__/StreamVideoClient.ringing.test.ts @@ -0,0 +1,185 @@ +import 'dotenv/config'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { StreamVideoClient } from '../StreamVideoClient'; +import { StreamClient } from '@stream-io/node-sdk'; +import { AllClientEvents } from '../coordinator/connection/types'; +import { RxUtils } from '../store'; +import { Call } from '../Call'; + +const apiKey = process.env.STREAM_API_KEY!; +const secret = process.env.STREAM_SECRET!; + +describe('StreamVideoClient Ringing', () => { + const serverClient = new StreamClient(apiKey, secret); + + let oliverClient: StreamVideoClient; + let sachaClient: StreamVideoClient; + let marceloClient: StreamVideoClient; + + beforeAll(async () => { + const makeClient = async (userId: string) => { + const client = new StreamVideoClient(apiKey, { + // tests run in node, so we have to fake being in browser env + browser: true, + timeout: 15000, + }); + await client.connectUser( + { id: userId }, + serverClient.generateUserToken({ user_id: userId }), + ); + return client; + }; + [oliverClient, sachaClient, marceloClient] = await Promise.all([ + makeClient('oliver'), + makeClient('sacha'), + makeClient('marcelo'), + ]); + }); + + afterAll(async () => { + await Promise.all([ + oliverClient.disconnectUser(), + sachaClient.disconnectUser(), + marceloClient.disconnectUser(), + ]); + }); + + describe('standard ringing', async () => { + it.each(['oliver', 'sara'])( + 'server-side: %s should ring all members, call creator should get call.ring event if present in members', + async (creatorId: string) => { + const oliverRing = expectEvent(oliverClient, 'call.ring'); + const sachaRing = expectEvent(sachaClient, 'call.ring'); + const marceloRing = expectEvent(marceloClient, 'call.ring'); + + const call = serverClient.video.call('default', crypto.randomUUID()); + await call.create({ + ring: true, + data: { + created_by_id: creatorId, + members: [ + { user_id: 'oliver' }, + { user_id: 'sacha' }, + { user_id: 'marcelo' }, + ], + }, + }); + + const [oliverRingEvent, sachaRingEvent, marceloRingEvent] = + await Promise.all([oliverRing, sachaRing, marceloRing]); + + expect(oliverRingEvent.call.cid).toBe(call.cid); + expect(sachaRingEvent.call.cid).toBe(call.cid); + expect(marceloRingEvent.call.cid).toBe(call.cid); + + const oliverCall = await expectCall(oliverClient, call.cid); + const sachaCall = await expectCall(sachaClient, call.cid); + const marceloCall = await expectCall(marceloClient, call.cid); + expect(oliverCall).toBeDefined(); + expect(sachaCall).toBeDefined(); + expect(marceloCall).toBeDefined(); + expect(oliverCall.ringing).toBe(true); + expect(sachaCall.ringing).toBe(true); + expect(marceloCall.ringing).toBe(true); + }, + ); + }); + + describe('ringing individual members', () => { + it('should ring individual members', async () => { + const oliverCall = oliverClient.call('default', crypto.randomUUID()); + await oliverCall.create({ + ring: false, // don't ring all members by default + data: { + members: [ + { user_id: 'oliver' }, + { user_id: 'sacha' }, + { user_id: 'marcelo' }, + ], + }, + }); + + // no one should get a ring event yet + const oliverRing = expectEvent(oliverClient, 'call.ring', 500); + const sachaRing = expectEvent(sachaClient, 'call.ring', 500); + const marceloRing = expectEvent(marceloClient, 'call.ring', 500); + await expect( + Promise.all([oliverRing, sachaRing, marceloRing]), + ).rejects.toThrow(); + + // oliver is calling sacha. only sacha should get a ring event + const sachaIndividualRing = expectEvent(sachaClient, 'call.ring'); + const marceloIndividualRing = expectEvent(marceloClient, 'call.ring'); + await oliverCall.ring({ target_member_ids: ['sacha'] }); + await expect(sachaIndividualRing).resolves.toHaveProperty( + 'call.cid', + oliverCall.cid, + ); + await expect(marceloIndividualRing).rejects.toThrow(); + + const sachaCall = await expectCall(sachaClient, oliverCall.cid); + expect(sachaCall).toBeDefined(); + + // sacha is calling marcelo. only marcelo should get a ring event + const oliverIndividualRing = expectEvent(oliverClient, 'call.ring'); + const marceloIndividualRing2 = expectEvent(marceloClient, 'call.ring'); + await sachaCall.ring({ target_member_ids: ['marcelo'] }); + await expect(marceloIndividualRing2).resolves.toHaveProperty( + 'call.cid', + sachaCall.cid, + ); + await expect(oliverIndividualRing).rejects.toThrow(); + + const marceloCall = await expectCall(marceloClient, sachaCall.cid); + expect(marceloCall).toBeDefined(); + }); + }); +}); + +const expectEvent = async ( + client: StreamVideoClient, + eventName: E, + timeout: number = 2500, +): Promise => { + return new Promise((resolve, reject) => { + let timeoutId: NodeJS.Timeout | undefined = undefined; + const off = client.on(eventName, (e) => { + off(); + clearTimeout(timeoutId); + resolve(e); + }); + timeoutId = setTimeout(() => { + off(); + reject( + new Error( + `Timeout waiting for event: ${eventName}, user_id: ${client.state.connectedUser?.id}`, + ), + ); + }, timeout); + }); +}; + +const expectCall = async ( + client: StreamVideoClient, + cid: string, + timeout: number = 2500, +) => { + return new Promise((resolve, reject) => { + let timeoutId: NodeJS.Timeout | undefined = undefined; + const off = RxUtils.createSubscription(client.state.calls$, (calls) => { + const call = calls.find((c) => c.cid === cid); + if (call) { + clearTimeout(timeoutId); + resolve(call); + } + }); + timeoutId = setTimeout(() => { + off(); + reject( + new Error( + `Timeout waiting for call: ${cid}, user_id: ${client.state.connectedUser?.id}`, + ), + ); + }, timeout); + }); +}; From 9cbcb941ebcc8c522f45135f1a0494ae9a6cdb84 Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Mon, 27 Oct 2025 17:28:40 +0100 Subject: [PATCH 06/10] restore husky --- .husky/pre-commit | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .husky/pre-commit diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000000..d2ae35e84b --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +yarn lint-staged From e4853503affb42bc18dd129e633cc2e599924c97 Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Mon, 27 Oct 2025 17:30:07 +0100 Subject: [PATCH 07/10] restore husky --- .husky/pre-commit | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 .husky/pre-commit diff --git a/.husky/pre-commit b/.husky/pre-commit old mode 100644 new mode 100755 From 09c7add8ae71167961cc84766052870565e8b31c Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Fri, 7 Nov 2025 17:13:58 +0100 Subject: [PATCH 08/10] feat: adjust to the new API --- packages/client/src/Call.ts | 21 +- .../StreamVideoClient.ringing.test.ts | 4 +- packages/client/src/gen/coordinator/index.ts | 718 +++++++++++++++++- 3 files changed, 714 insertions(+), 29 deletions(-) diff --git a/packages/client/src/Call.ts b/packages/client/src/Call.ts index 5f0475cf2e..e1d7ea0ca7 100644 --- a/packages/client/src/Call.ts +++ b/packages/client/src/Call.ts @@ -64,6 +64,8 @@ import type { RejectCallResponse, RequestPermissionRequest, RequestPermissionResponse, + RingCallRequest, + RingCallResponse, SendCallEventRequest, SendCallEventResponse, SendReactionRequest, @@ -732,19 +734,17 @@ export class Call { * @param params.notify if set to true, a `call.notification` event will be sent to the call members. * @param params.members_limit the total number of members to return as part of the response. * @param params.video if set to true, in a ringing scenario, mobile SDKs will show "incoming video call", audio only otherwise. - * @param params.target_member_ids the list of members to ring. Limited to 100 members per request. */ get = async (params?: { ring?: boolean; notify?: boolean; members_limit?: number; video?: boolean; - target_member_ids?: string[]; }): Promise => { await this.setup(); const response = await this.streamClient.get( this.streamClientBasePath, - { ...params, target_member_ids: params?.target_member_ids?.join(',') }, + params, ); this.state.updateFromCallResponse(response.call); @@ -817,15 +817,14 @@ export class Call { }; /** - * A shortcut for {@link Call.get} with `ring` parameter set to `true`. - * Will send a `call.ring` event to the call members. - * - * @param params.member_ids the list of members to ring. Limited to 100 members per request. + * Sends a ring notification to the provided users who are not already in the call. + * All users should be members of the call. */ - ring = async (params: { - target_member_ids?: string[]; - }): Promise => { - return await this.get({ ...params, ring: true }); + ring = async (data: RingCallRequest = {}): Promise => { + return this.streamClient.post( + `${this.streamClientBasePath}/ring`, + data, + ); }; /** diff --git a/packages/client/src/__tests__/StreamVideoClient.ringing.test.ts b/packages/client/src/__tests__/StreamVideoClient.ringing.test.ts index ca5e0c70f8..1900c58fb1 100644 --- a/packages/client/src/__tests__/StreamVideoClient.ringing.test.ts +++ b/packages/client/src/__tests__/StreamVideoClient.ringing.test.ts @@ -145,7 +145,7 @@ describe('StreamVideoClient Ringing', () => { // oliver is calling sacha. only sacha should get a ring event const sachaIndividualRing = expectEvent(sachaClient, 'call.ring'); const marceloIndividualRing = expectEvent(marceloClient, 'call.ring'); - await oliverCall.ring({ target_member_ids: ['sacha'] }); + await oliverCall.ring({ members_ids: ['sacha'] }); await expect(sachaIndividualRing).resolves.toHaveProperty( 'call.cid', oliverCall.cid, @@ -158,7 +158,7 @@ describe('StreamVideoClient Ringing', () => { // sacha is calling marcelo. only marcelo should get a ring event const oliverIndividualRing = expectEvent(oliverClient, 'call.ring'); const marceloIndividualRing2 = expectEvent(marceloClient, 'call.ring'); - await sachaCall.ring({ target_member_ids: ['marcelo'] }); + await sachaCall.ring({ members_ids: ['marcelo'] }); await expect(marceloIndividualRing2).resolves.toHaveProperty( 'call.cid', sachaCall.cid, diff --git a/packages/client/src/gen/coordinator/index.ts b/packages/client/src/gen/coordinator/index.ts index 16ac947ab2..37a8f0ef52 100644 --- a/packages/client/src/gen/coordinator/index.ts +++ b/packages/client/src/gen/coordinator/index.ts @@ -1207,7 +1207,7 @@ export interface CallMissedEvent { user: UserResponse; } /** - * + * This event is sent when a moderation blur action is applied to a user's video stream * @export * @interface CallModerationBlurEvent */ @@ -1225,26 +1225,26 @@ export interface CallModerationBlurEvent { */ created_at: string; /** - * + * Custom data associated with the moderation action * @type {{ [key: string]: any; }} * @memberof CallModerationBlurEvent */ custom: { [key: string]: any }; /** - * + * The type of event: "call.moderation_blur" in this case * @type {string} * @memberof CallModerationBlurEvent */ type: string; /** - * + * The user ID whose video stream is being blurred * @type {string} * @memberof CallModerationBlurEvent */ user_id: string; } /** - * + * This event is sent when a moderation warning is issued to a user * @export * @interface CallModerationWarningEvent */ @@ -1262,25 +1262,25 @@ export interface CallModerationWarningEvent { */ created_at: string; /** - * + * Custom data associated with the moderation action * @type {{ [key: string]: any; }} * @memberof CallModerationWarningEvent */ custom: { [key: string]: any }; /** - * + * The warning message * @type {string} * @memberof CallModerationWarningEvent */ message: string; /** - * + * The type of event: "call.moderation_warning" in this case * @type {string} * @memberof CallModerationWarningEvent */ type: string; /** - * + * The user ID who is receiving the warning * @type {string} * @memberof CallModerationWarningEvent */ @@ -2538,6 +2538,55 @@ export interface CallStateResponseFields { */ own_capabilities: Array; } +/** + * + * @export + * @interface CallStatsLocation + */ +export interface CallStatsLocation { + /** + * + * @type {number} + * @memberof CallStatsLocation + */ + accuracy_radius_meters?: number; + /** + * + * @type {string} + * @memberof CallStatsLocation + */ + city?: string; + /** + * + * @type {string} + * @memberof CallStatsLocation + */ + continent?: string; + /** + * + * @type {string} + * @memberof CallStatsLocation + */ + country?: string; + /** + * + * @type {number} + * @memberof CallStatsLocation + */ + latitude?: number; + /** + * + * @type {number} + * @memberof CallStatsLocation + */ + longitude?: number; + /** + * + * @type {string} + * @memberof CallStatsLocation + */ + subdivision?: string; +} /** * * @export @@ -2593,6 +2642,18 @@ export interface CallStatsParticipantCounts { * @memberof CallStatsParticipantCounts */ participants: number; + /** + * + * @type {number} + * @memberof CallStatsParticipantCounts + */ + peak_concurrent_sessions: number; + /** + * + * @type {number} + * @memberof CallStatsParticipantCounts + */ + peak_concurrent_users: number; /** * * @type {number} @@ -2612,12 +2673,42 @@ export interface CallStatsParticipantCounts { * @interface CallStatsParticipantSession */ export interface CallStatsParticipantSession { + /** + * + * @type {string} + * @memberof CallStatsParticipantSession + */ + browser?: string; + /** + * + * @type {string} + * @memberof CallStatsParticipantSession + */ + browser_version?: string; /** * * @type {number} * @memberof CallStatsParticipantSession */ cq_score?: number; + /** + * + * @type {string} + * @memberof CallStatsParticipantSession + */ + current_ip?: string; + /** + * + * @type {string} + * @memberof CallStatsParticipantSession + */ + current_sfu?: string; + /** + * + * @type {number} + * @memberof CallStatsParticipantSession + */ + distance_to_sfu_kilometers?: number; /** * * @type {string} @@ -2630,6 +2721,18 @@ export interface CallStatsParticipantSession { * @memberof CallStatsParticipantSession */ is_live: boolean; + /** + * + * @type {CallStatsLocation} + * @memberof CallStatsParticipantSession + */ + location?: CallStatsLocation; + /** + * + * @type {string} + * @memberof CallStatsParticipantSession + */ + os?: string; /** * * @type {PublishedTrackFlags} @@ -2642,6 +2745,18 @@ export interface CallStatsParticipantSession { * @memberof CallStatsParticipantSession */ publisher_type?: string; + /** + * + * @type {string} + * @memberof CallStatsParticipantSession + */ + sdk?: string; + /** + * + * @type {string} + * @memberof CallStatsParticipantSession + */ + sdk_version?: string; /** * * @type {string} @@ -2660,6 +2775,12 @@ export interface CallStatsParticipantSession { * @memberof CallStatsParticipantSession */ user_session_id: string; + /** + * + * @type {string} + * @memberof CallStatsParticipantSession + */ + webrtc_version?: string; } /** * This event is sent when the insights report is ready @@ -3921,7 +4042,6 @@ export const FrameRecordingSettingsRequestQualityEnum = { _720P: '720p', _1080P: '1080p', _1440P: '1440p', - _2160P: '2160p', } as const; export type FrameRecordingSettingsRequestQualityEnum = (typeof FrameRecordingSettingsRequestQualityEnum)[keyof typeof FrameRecordingSettingsRequestQualityEnum]; @@ -5167,6 +5287,31 @@ export interface MessageStatsResponse { */ count_over_time?: Array; } +/** + * + * @export + * @interface MetricDescriptor + */ +export interface MetricDescriptor { + /** + * + * @type {string} + * @memberof MetricDescriptor + */ + description?: string; + /** + * + * @type {string} + * @memberof MetricDescriptor + */ + label: string; + /** + * + * @type {string} + * @memberof MetricDescriptor + */ + unit?: string; +} /** * * @export @@ -5606,6 +5751,18 @@ export interface ParticipantSeriesPublisherStats { * @memberof ParticipantSeriesPublisherStats */ global?: { [key: string]: Array> }; + /** + * + * @type {{ [key: string]: MetricDescriptor; }} + * @memberof ParticipantSeriesPublisherStats + */ + global_meta?: { [key: string]: MetricDescriptor }; + /** + * + * @type {Array} + * @memberof ParticipantSeriesPublisherStats + */ + global_metrics_order?: Array; /** * * @type {{ [key: string]: Array; }} @@ -5631,6 +5788,18 @@ export interface ParticipantSeriesSubscriberStats { * @memberof ParticipantSeriesSubscriberStats */ global?: { [key: string]: Array> }; + /** + * + * @type {{ [key: string]: MetricDescriptor; }} + * @memberof ParticipantSeriesSubscriberStats + */ + global_meta?: { [key: string]: MetricDescriptor }; + /** + * + * @type {Array} + * @memberof ParticipantSeriesSubscriberStats + */ + global_metrics_order?: Array; /** * * @type {{ [key: string]: Array; }} @@ -5730,6 +5899,18 @@ export interface ParticipantSeriesTrackMetrics { * @memberof ParticipantSeriesTrackMetrics */ metrics?: { [key: string]: Array> }; + /** + * + * @type {{ [key: string]: MetricDescriptor; }} + * @memberof ParticipantSeriesTrackMetrics + */ + metrics_meta?: { [key: string]: MetricDescriptor }; + /** + * + * @type {Array} + * @memberof ParticipantSeriesTrackMetrics + */ + metrics_order?: Array; /** * * @type {string} @@ -5767,6 +5948,18 @@ export interface ParticipantSeriesUserStats { * @memberof ParticipantSeriesUserStats */ metrics?: { [key: string]: Array> }; + /** + * + * @type {{ [key: string]: MetricDescriptor; }} + * @memberof ParticipantSeriesUserStats + */ + metrics_meta?: { [key: string]: MetricDescriptor }; + /** + * + * @type {Array} + * @memberof ParticipantSeriesUserStats + */ + metrics_order?: Array; /** * * @type {{ [key: string]: Array; }} @@ -6503,13 +6696,11 @@ export const RTMPBroadcastRequestQualityEnum = { _720P: '720p', _1080P: '1080p', _1440P: '1440p', - _2160P: '2160p', PORTRAIT_360X640: 'portrait-360x640', PORTRAIT_480X854: 'portrait-480x854', PORTRAIT_720X1280: 'portrait-720x1280', PORTRAIT_1080X1920: 'portrait-1080x1920', PORTRAIT_1440X2560: 'portrait-1440x2560', - PORTRAIT_2160X3840: 'portrait-2160x3840', } as const; export type RTMPBroadcastRequestQualityEnum = (typeof RTMPBroadcastRequestQualityEnum)[keyof typeof RTMPBroadcastRequestQualityEnum]; @@ -6556,13 +6747,11 @@ export const RTMPSettingsRequestQualityEnum = { _720P: '720p', _1080P: '1080p', _1440P: '1440p', - _2160P: '2160p', PORTRAIT_360X640: 'portrait-360x640', PORTRAIT_480X854: 'portrait-480x854', PORTRAIT_720X1280: 'portrait-720x1280', PORTRAIT_1080X1920: 'portrait-1080x1920', PORTRAIT_1440X2560: 'portrait-1440x2560', - PORTRAIT_2160X3840: 'portrait-2160x3840', } as const; export type RTMPSettingsRequestQualityEnum = (typeof RTMPSettingsRequestQualityEnum)[keyof typeof RTMPSettingsRequestQualityEnum]; @@ -6663,13 +6852,11 @@ export const RecordSettingsRequestQualityEnum = { _720P: '720p', _1080P: '1080p', _1440P: '1440p', - _2160P: '2160p', PORTRAIT_360X640: 'portrait-360x640', PORTRAIT_480X854: 'portrait-480x854', PORTRAIT_720X1280: 'portrait-720x1280', PORTRAIT_1080X1920: 'portrait-1080x1920', PORTRAIT_1440X2560: 'portrait-1440x2560', - PORTRAIT_2160X3840: 'portrait-2160x3840', } as const; export type RecordSettingsRequestQualityEnum = (typeof RecordSettingsRequestQualityEnum)[keyof typeof RecordSettingsRequestQualityEnum]; @@ -6813,6 +7000,68 @@ export interface RequestPermissionResponse { */ duration: string; } +/** + * Request to resolve SIP inbound routing using challenge authentication + * @export + * @interface ResolveSipInboundRequest + */ +export interface ResolveSipInboundRequest { + /** + * + * @type {SIPChallenge} + * @memberof ResolveSipInboundRequest + */ + challenge: SIPChallenge; + /** + * SIP caller number + * @type {string} + * @memberof ResolveSipInboundRequest + */ + sip_caller_number: string; + /** + * Optional SIP headers as key-value pairs + * @type {{ [key: string]: string; }} + * @memberof ResolveSipInboundRequest + */ + sip_headers?: { [key: string]: string }; + /** + * SIP trunk number to resolve + * @type {string} + * @memberof ResolveSipInboundRequest + */ + sip_trunk_number: string; +} +/** + * Response containing resolved SIP inbound routing information + * @export + * @interface ResolveSipInboundResponse + */ +export interface ResolveSipInboundResponse { + /** + * + * @type {SipInboundCredentials} + * @memberof ResolveSipInboundResponse + */ + credentials: SipInboundCredentials; + /** + * + * @type {string} + * @memberof ResolveSipInboundResponse + */ + duration: string; + /** + * + * @type {SIPInboundRoutingRuleResponse} + * @memberof ResolveSipInboundResponse + */ + sip_routing_rule?: SIPInboundRoutingRuleResponse; + /** + * + * @type {SIPTrunkResponse} + * @memberof ResolveSipInboundResponse + */ + sip_trunk?: SIPTrunkResponse; +} /** * Basic response information * @export @@ -6826,6 +7075,44 @@ export interface Response { */ duration: string; } +/** + * + * @export + * @interface RingCallRequest + */ +export interface RingCallRequest { + /** + * Members that should receive the ring. If no ids are provided, all call members who are not already in the call will receive ring notifications. + * @type {Array} + * @memberof RingCallRequest + */ + members_ids?: Array; + /** + * Indicate if call should be video + * @type {boolean} + * @memberof RingCallRequest + */ + video?: boolean; +} +/** + * + * @export + * @interface RingCallResponse + */ +export interface RingCallResponse { + /** + * + * @type {string} + * @memberof RingCallResponse + */ + duration: string; + /** + * List of members ringing notification was sent to + * @type {Array} + * @memberof RingCallResponse + */ + members_ids: Array; +} /** * * @export @@ -6927,6 +7214,362 @@ export interface SFUResponse { */ ws_endpoint: string; } +/** + * SIP call configuration response + * @export + * @interface SIPCallConfigsResponse + */ +export interface SIPCallConfigsResponse { + /** + * Custom data associated with the call + * @type {{ [key: string]: any; }} + * @memberof SIPCallConfigsResponse + */ + custom_data: { [key: string]: any }; +} +/** + * SIP caller configuration response + * @export + * @interface SIPCallerConfigsResponse + */ +export interface SIPCallerConfigsResponse { + /** + * Custom data associated with the caller + * @type {{ [key: string]: any; }} + * @memberof SIPCallerConfigsResponse + */ + custom_data: { [key: string]: any }; + /** + * Unique identifier for the caller + * @type {string} + * @memberof SIPCallerConfigsResponse + */ + id: string; +} +/** + * + * @export + * @interface SIPChallenge + */ +export interface SIPChallenge { + /** + * + * @type {string} + * @memberof SIPChallenge + */ + a1?: string; + /** + * + * @type {string} + * @memberof SIPChallenge + */ + algorithm?: string; + /** + * + * @type {string} + * @memberof SIPChallenge + */ + charset?: string; + /** + * + * @type {string} + * @memberof SIPChallenge + */ + cnonce?: string; + /** + * + * @type {Array} + * @memberof SIPChallenge + */ + domain?: Array; + /** + * + * @type {string} + * @memberof SIPChallenge + */ + method?: string; + /** + * + * @type {string} + * @memberof SIPChallenge + */ + nc?: string; + /** + * + * @type {string} + * @memberof SIPChallenge + */ + nonce?: string; + /** + * + * @type {string} + * @memberof SIPChallenge + */ + opaque?: string; + /** + * + * @type {Array} + * @memberof SIPChallenge + */ + qop?: Array; + /** + * + * @type {string} + * @memberof SIPChallenge + */ + realm?: string; + /** + * + * @type {string} + * @memberof SIPChallenge + */ + response?: string; + /** + * + * @type {boolean} + * @memberof SIPChallenge + */ + stale?: boolean; + /** + * + * @type {string} + * @memberof SIPChallenge + */ + uri?: string; + /** + * + * @type {boolean} + * @memberof SIPChallenge + */ + userhash?: boolean; + /** + * + * @type {string} + * @memberof SIPChallenge + */ + username?: string; +} +/** + * Direct routing rule call configuration response + * @export + * @interface SIPDirectRoutingRuleCallConfigsResponse + */ +export interface SIPDirectRoutingRuleCallConfigsResponse { + /** + * ID of the call + * @type {string} + * @memberof SIPDirectRoutingRuleCallConfigsResponse + */ + call_id: string; + /** + * Type of the call + * @type {string} + * @memberof SIPDirectRoutingRuleCallConfigsResponse + */ + call_type: string; +} +/** + * PIN routing rule call configuration response + * @export + * @interface SIPInboundRoutingRulePinConfigsResponse + */ +export interface SIPInboundRoutingRulePinConfigsResponse { + /** + * Optional webhook URL for custom PIN handling + * @type {string} + * @memberof SIPInboundRoutingRulePinConfigsResponse + */ + custom_webhook_url?: string; + /** + * Prompt message for failed PIN attempts + * @type {string} + * @memberof SIPInboundRoutingRulePinConfigsResponse + */ + pin_failed_attempt_prompt?: string; + /** + * Prompt message for hangup after PIN input + * @type {string} + * @memberof SIPInboundRoutingRulePinConfigsResponse + */ + pin_hangup_prompt?: string; + /** + * Prompt message for PIN input + * @type {string} + * @memberof SIPInboundRoutingRulePinConfigsResponse + */ + pin_prompt?: string; + /** + * Prompt message for successful PIN input + * @type {string} + * @memberof SIPInboundRoutingRulePinConfigsResponse + */ + pin_success_prompt?: string; +} +/** + * SIP Inbound Routing Rule response + * @export + * @interface SIPInboundRoutingRuleResponse + */ +export interface SIPInboundRoutingRuleResponse { + /** + * + * @type {SIPCallConfigsResponse} + * @memberof SIPInboundRoutingRuleResponse + */ + call_configs?: SIPCallConfigsResponse; + /** + * List of called numbers + * @type {Array} + * @memberof SIPInboundRoutingRuleResponse + */ + called_numbers: Array; + /** + * + * @type {SIPCallerConfigsResponse} + * @memberof SIPInboundRoutingRuleResponse + */ + caller_configs?: SIPCallerConfigsResponse; + /** + * List of caller numbers + * @type {Array} + * @memberof SIPInboundRoutingRuleResponse + */ + caller_numbers?: Array; + /** + * + * @type {SIPDirectRoutingRuleCallConfigsResponse} + * @memberof SIPInboundRoutingRuleResponse + */ + direct_routing_configs?: SIPDirectRoutingRuleCallConfigsResponse; + /** + * + * @type {string} + * @memberof SIPInboundRoutingRuleResponse + */ + duration: string; + /** + * Unique identifier of the SIP Inbound Routing Rule + * @type {string} + * @memberof SIPInboundRoutingRuleResponse + */ + id: string; + /** + * Name of the SIP Inbound Routing Rule + * @type {string} + * @memberof SIPInboundRoutingRuleResponse + */ + name: string; + /** + * + * @type {SIPPinProtectionConfigsResponse} + * @memberof SIPInboundRoutingRuleResponse + */ + pin_protection_configs?: SIPPinProtectionConfigsResponse; + /** + * + * @type {SIPInboundRoutingRulePinConfigsResponse} + * @memberof SIPInboundRoutingRuleResponse + */ + pin_routing_configs?: SIPInboundRoutingRulePinConfigsResponse; + /** + * List of SIP trunk IDs + * @type {Array} + * @memberof SIPInboundRoutingRuleResponse + */ + trunk_ids: Array; + /** + * + * @type {object} + * @memberof SIPInboundRoutingRuleResponse + */ + updated_at: object; +} +/** + * PIN protection configuration response + * @export + * @interface SIPPinProtectionConfigsResponse + */ +export interface SIPPinProtectionConfigsResponse { + /** + * Default PIN to use if there is no PIN set on the call object + * @type {string} + * @memberof SIPPinProtectionConfigsResponse + */ + default_pin?: string; + /** + * Whether PIN protection is enabled + * @type {boolean} + * @memberof SIPPinProtectionConfigsResponse + */ + enabled: boolean; + /** + * Maximum number of PIN attempts allowed + * @type {number} + * @memberof SIPPinProtectionConfigsResponse + */ + max_attempts?: number; + /** + * Number of digits required for the PIN + * @type {number} + * @memberof SIPPinProtectionConfigsResponse + */ + required_pin_digits?: number; +} +/** + * SIP trunk information + * @export + * @interface SIPTrunkResponse + */ +export interface SIPTrunkResponse { + /** + * + * @type {object} + * @memberof SIPTrunkResponse + */ + created_at: object; + /** + * Unique identifier for the SIP trunk + * @type {string} + * @memberof SIPTrunkResponse + */ + id: string; + /** + * Name of the SIP trunk + * @type {string} + * @memberof SIPTrunkResponse + */ + name: string; + /** + * Phone numbers associated with this SIP trunk + * @type {Array} + * @memberof SIPTrunkResponse + */ + numbers: Array; + /** + * Password for SIP trunk authentication + * @type {string} + * @memberof SIPTrunkResponse + */ + password: string; + /** + * + * @type {object} + * @memberof SIPTrunkResponse + */ + updated_at: object; + /** + * The URI for the SIP trunk + * @type {string} + * @memberof SIPTrunkResponse + */ + uri: string; + /** + * Username for SIP trunk authentication + * @type {string} + * @memberof SIPTrunkResponse + */ + username: string; +} /** * * @export @@ -7086,6 +7729,49 @@ export interface SessionSettingsResponse { */ inactivity_timeout_seconds: number; } +/** + * Credentials for SIP inbound call authentication + * @export + * @interface SipInboundCredentials + */ +export interface SipInboundCredentials { + /** + * Custom data associated with the call + * @type {{ [key: string]: any; }} + * @memberof SipInboundCredentials + */ + call_custom_data: { [key: string]: any }; + /** + * ID of the call + * @type {string} + * @memberof SipInboundCredentials + */ + call_id: string; + /** + * Type of the call + * @type {string} + * @memberof SipInboundCredentials + */ + call_type: string; + /** + * Authentication token for the call + * @type {string} + * @memberof SipInboundCredentials + */ + token: string; + /** + * Custom data associated with the user + * @type {{ [key: string]: any; }} + * @memberof SipInboundCredentials + */ + user_custom_data: { [key: string]: any }; + /** + * User ID for the call + * @type {string} + * @memberof SipInboundCredentials + */ + user_id: string; +} /** * * @export From 8f7079f50a28f9f3c67cf4e43885faba5a541cc8 Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Mon, 10 Nov 2025 17:04:37 +0100 Subject: [PATCH 09/10] fix: don't reject calls that are created by the current user --- packages/client/src/StreamVideoClient.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/client/src/StreamVideoClient.ts b/packages/client/src/StreamVideoClient.ts index 6dae9b01d9..bbbb451bde 100644 --- a/packages/client/src/StreamVideoClient.ts +++ b/packages/client/src/StreamVideoClient.ts @@ -592,16 +592,14 @@ export class StreamVideoClient { private shouldRejectCall = (currentCallId: string) => { if (!this.rejectCallWhenBusy) return false; - - const hasOngoingRingingCall = this.state.calls.some( + return this.state.calls.some( (c) => c.cid !== currentCallId && c.ringing && + !c.isCreatedByMe && c.state.callingState !== CallingState.IDLE && c.state.callingState !== CallingState.LEFT && c.state.callingState !== CallingState.RECONNECTING_FAILED, ); - - return hasOngoingRingingCall; }; } From 848b21bad358dcfd1d89d2e69d150ad1c6b1df3f Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Mon, 10 Nov 2025 17:22:33 +0100 Subject: [PATCH 10/10] adjust the test according to the latest spec --- .../src/__tests__/StreamVideoClient.ringing.test.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/client/src/__tests__/StreamVideoClient.ringing.test.ts b/packages/client/src/__tests__/StreamVideoClient.ringing.test.ts index 1900c58fb1..d7b28bbbfa 100644 --- a/packages/client/src/__tests__/StreamVideoClient.ringing.test.ts +++ b/packages/client/src/__tests__/StreamVideoClient.ringing.test.ts @@ -100,21 +100,20 @@ describe('StreamVideoClient Ringing', () => { ], }, }); + expect(call.ringing).toBe(true); - const [oliverRingEvent, sachaRingEvent, marceloRingEvent] = - await Promise.all([oliverRing, sachaRing, marceloRing]); + const ringEventsPromise = Promise.all([sachaRing, marceloRing]); + await expect(ringEventsPromise).resolves.toHaveLength(2); + await expect(oliverRing).rejects.toThrow(); // caller doesn't get ring event + const [sachaRingEvent, marceloRingEvent] = await ringEventsPromise; - expect(oliverRingEvent.call.cid).toBe(call.cid); expect(sachaRingEvent.call.cid).toBe(call.cid); expect(marceloRingEvent.call.cid).toBe(call.cid); - const oliverCall = await expectCall(oliverClient, call.cid); const sachaCall = await expectCall(sachaClient, call.cid); const marceloCall = await expectCall(marceloClient, call.cid); - expect(oliverCall).toBeDefined(); expect(sachaCall).toBeDefined(); expect(marceloCall).toBeDefined(); - expect(oliverCall.ringing).toBe(true); expect(sachaCall.ringing).toBe(true); expect(marceloCall.ringing).toBe(true); });