@stream-io/video-client 1.36.1 → 1.37.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -295,6 +295,15 @@ export declare class CallState {
295
295
  * The stable list of participants in the current call, unsorted.
296
296
  */
297
297
  get rawParticipants(): StreamVideoParticipant[];
298
+ /**
299
+ * Returns the current participants array directly from the BehaviorSubject.
300
+ * This bypasses the observable pipeline and is guaranteed to be synchronous.
301
+ * Use this when you need the absolute latest value without any potential
302
+ * timing issues from shareReplay/refCount.
303
+ *
304
+ * @internal
305
+ */
306
+ getParticipantsSnapshot: () => StreamVideoParticipant[];
298
307
  /**
299
308
  * Sets the list of participants in the current call.
300
309
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-client",
3
- "version": "1.36.1",
3
+ "version": "1.37.1",
4
4
  "main": "dist/index.cjs.js",
5
5
  "module": "dist/index.es.js",
6
6
  "browser": "dist/index.browser.es.js",
package/src/Call.ts CHANGED
@@ -64,6 +64,8 @@ import type {
64
64
  RejectCallResponse,
65
65
  RequestPermissionRequest,
66
66
  RequestPermissionResponse,
67
+ RingCallRequest,
68
+ RingCallResponse,
67
69
  SendCallEventRequest,
68
70
  SendCallEventResponse,
69
71
  SendReactionRequest,
@@ -731,12 +733,14 @@ export class Call {
731
733
  * @param params.ring if set to true, a `call.ring` event will be sent to the call members.
732
734
  * @param params.notify if set to true, a `call.notification` event will be sent to the call members.
733
735
  * @param params.members_limit the total number of members to return as part of the response.
736
+ * @param params.video if set to true, in a ringing scenario, mobile SDKs will show "incoming video call", audio only otherwise.
734
737
  */
735
738
  get = async (params?: {
736
739
  ring?: boolean;
737
740
  notify?: boolean;
738
741
  members_limit?: number;
739
- }) => {
742
+ video?: boolean;
743
+ }): Promise<GetCallResponse> => {
740
744
  await this.setup();
741
745
  const response = await this.streamClient.get<GetCallResponse>(
742
746
  this.streamClientBasePath,
@@ -813,11 +817,14 @@ export class Call {
813
817
  };
814
818
 
815
819
  /**
816
- * A shortcut for {@link Call.get} with `ring` parameter set to `true`.
817
- * Will send a `call.ring` event to the call members.
820
+ * Sends a ring notification to the provided users who are not already in the call.
821
+ * All users should be members of the call.
818
822
  */
819
- ring = async (): Promise<GetCallResponse> => {
820
- return await this.get({ ring: true });
823
+ ring = async (data: RingCallRequest = {}): Promise<RingCallResponse> => {
824
+ return this.streamClient.post<RingCallResponse, RingCallRequest>(
825
+ `${this.streamClientBasePath}/ring`,
826
+ data,
827
+ );
821
828
  };
822
829
 
823
830
  /**
@@ -199,11 +199,6 @@ export class StreamVideoClient {
199
199
  * @param e the event.
200
200
  */
201
201
  private initCallFromEvent = async (e: CallCreatedEvent | CallRingEvent) => {
202
- if (this.state.connectedUser?.id === e.call.created_by.id) {
203
- this.logger.debug(`Ignoring ${e.type} event sent by the current user`);
204
- return;
205
- }
206
-
207
202
  try {
208
203
  const concurrencyTag = getCallInitConcurrencyTag(e.call_cid);
209
204
  await withoutConcurrency(concurrencyTag, async () => {
@@ -597,16 +592,14 @@ export class StreamVideoClient {
597
592
 
598
593
  private shouldRejectCall = (currentCallId: string) => {
599
594
  if (!this.rejectCallWhenBusy) return false;
600
-
601
- const hasOngoingRingingCall = this.state.calls.some(
595
+ return this.state.calls.some(
602
596
  (c) =>
603
597
  c.cid !== currentCallId &&
604
598
  c.ringing &&
599
+ !c.isCreatedByMe &&
605
600
  c.state.callingState !== CallingState.IDLE &&
606
601
  c.state.callingState !== CallingState.LEFT &&
607
602
  c.state.callingState !== CallingState.RECONNECTING_FAILED,
608
603
  );
609
-
610
- return hasOngoingRingingCall;
611
604
  };
612
605
  }
@@ -10,34 +10,20 @@ const secret = process.env.STREAM_SECRET!;
10
10
 
11
11
  const serverClient = new StreamClient(apiKey, secret);
12
12
 
13
- const tokenProvider = (userId: string) => {
14
- return async () => {
15
- return new Promise<string>((resolve) => {
16
- setTimeout(() => {
17
- const token = serverClient.createToken(
18
- userId,
19
- undefined,
20
- Math.round(Date.now() / 1000 - 10),
21
- );
22
- resolve(token);
23
- }, 100);
24
- });
25
- };
26
- };
27
-
28
13
  describe('StreamVideoClient - coordinator API', () => {
29
14
  let client: StreamVideoClient;
30
- const user = {
31
- id: 'sara',
32
- };
33
15
 
34
16
  beforeAll(() => {
17
+ const user = { id: 'sara' };
35
18
  client = new StreamVideoClient(apiKey, {
36
19
  // tests run in node, so we have to fake being in browser env
37
20
  browser: true,
38
21
  timeout: 15000,
39
22
  });
40
- client.connectUser(user, tokenProvider(user.id));
23
+ client.connectUser(
24
+ user,
25
+ serverClient.generateUserToken({ user_id: user.id }),
26
+ );
41
27
  });
42
28
 
43
29
  it('query calls', { retry: 3, timeout: 20000 }, async () => {
@@ -100,26 +100,75 @@ describe('StreamVideoClient Ringing', () => {
100
100
  ],
101
101
  },
102
102
  });
103
+ expect(call.ringing).toBe(true);
103
104
 
104
- const [oliverRingEvent, sachaRingEvent, marceloRingEvent] =
105
- await Promise.all([oliverRing, sachaRing, marceloRing]);
105
+ const ringEventsPromise = Promise.all([sachaRing, marceloRing]);
106
+ await expect(ringEventsPromise).resolves.toHaveLength(2);
107
+ await expect(oliverRing).rejects.toThrow(); // caller doesn't get ring event
108
+ const [sachaRingEvent, marceloRingEvent] = await ringEventsPromise;
106
109
 
107
- expect(oliverRingEvent.call.cid).toBe(call.cid);
108
110
  expect(sachaRingEvent.call.cid).toBe(call.cid);
109
111
  expect(marceloRingEvent.call.cid).toBe(call.cid);
110
112
 
111
- const oliverCall = await expectCall(oliverClient, call.cid);
112
113
  const sachaCall = await expectCall(sachaClient, call.cid);
113
114
  const marceloCall = await expectCall(marceloClient, call.cid);
114
- expect(oliverCall).toBeDefined();
115
115
  expect(sachaCall).toBeDefined();
116
116
  expect(marceloCall).toBeDefined();
117
- expect(oliverCall.ringing).toBe(true);
118
117
  expect(sachaCall.ringing).toBe(true);
119
118
  expect(marceloCall.ringing).toBe(true);
120
119
  });
121
120
  });
122
121
 
122
+ describe('ringing individual members', () => {
123
+ it('should ring individual members', async () => {
124
+ const oliverCall = oliverClient.call('default', crypto.randomUUID());
125
+ await oliverCall.create({
126
+ ring: false, // don't ring all members by default
127
+ data: {
128
+ members: [
129
+ { user_id: 'oliver' },
130
+ { user_id: 'sacha' },
131
+ { user_id: 'marcelo' },
132
+ ],
133
+ },
134
+ });
135
+
136
+ // no one should get a ring event yet
137
+ const oliverRing = expectEvent(oliverClient, 'call.ring', 500);
138
+ const sachaRing = expectEvent(sachaClient, 'call.ring', 500);
139
+ const marceloRing = expectEvent(marceloClient, 'call.ring', 500);
140
+ await expect(
141
+ Promise.all([oliverRing, sachaRing, marceloRing]),
142
+ ).rejects.toThrow();
143
+
144
+ // oliver is calling sacha. only sacha should get a ring event
145
+ const sachaIndividualRing = expectEvent(sachaClient, 'call.ring');
146
+ const marceloIndividualRing = expectEvent(marceloClient, 'call.ring');
147
+ await oliverCall.ring({ members_ids: ['sacha'] });
148
+ await expect(sachaIndividualRing).resolves.toHaveProperty(
149
+ 'call.cid',
150
+ oliverCall.cid,
151
+ );
152
+ await expect(marceloIndividualRing).rejects.toThrow();
153
+
154
+ const sachaCall = await expectCall(sachaClient, oliverCall.cid);
155
+ expect(sachaCall).toBeDefined();
156
+
157
+ // sacha is calling marcelo. only marcelo should get a ring event
158
+ const oliverIndividualRing = expectEvent(oliverClient, 'call.ring');
159
+ const marceloIndividualRing2 = expectEvent(marceloClient, 'call.ring');
160
+ await sachaCall.ring({ members_ids: ['marcelo'] });
161
+ await expect(marceloIndividualRing2).resolves.toHaveProperty(
162
+ 'call.cid',
163
+ sachaCall.cid,
164
+ );
165
+ await expect(oliverIndividualRing).rejects.toThrow();
166
+
167
+ const marceloCall = await expectCall(marceloClient, sachaCall.cid);
168
+ expect(marceloCall).toBeDefined();
169
+ });
170
+ });
171
+
123
172
  describe('ringing concurrently', async () => {
124
173
  it('dispatches `call.ring` before `call.created`', async () => {
125
174
  oliverClient.streamClient.dispatchEvent(