@stream-io/video-client 0.0.2-alpha.9 → 0.0.4

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.
@@ -10,7 +10,10 @@ import { OwnCapability } from '../gen/coordinator';
10
10
  export const watchCallPermissionRequest = (state: CallState) => {
11
11
  return function onCallPermissionRequest(event: StreamVideoEvent) {
12
12
  if (event.type !== 'call.permission_request') return;
13
- state.setCallPermissionRequest(event);
13
+ const { localParticipant } = state;
14
+ if (event.user.id !== localParticipant?.userId) {
15
+ state.setCallPermissionRequest(event);
16
+ }
14
17
  };
15
18
  };
16
19
 
@@ -22,10 +25,7 @@ export const watchCallPermissionsUpdated = (state: CallState) => {
22
25
  if (event.type !== 'call.permissions_updated') return;
23
26
  const { localParticipant } = state;
24
27
  if (event.user.id === localParticipant?.userId) {
25
- state.setMetadata((metadata) => ({
26
- ...metadata!,
27
- own_capabilities: event.own_capabilities,
28
- }));
28
+ state.setOwnCapabilities(event.own_capabilities);
29
29
  }
30
30
  };
31
31
  };
@@ -49,7 +49,7 @@ export const watchCallGrantsUpdated = (state: CallState) => {
49
49
  [OwnCapability.SCREENSHARE]: canScreenshare,
50
50
  };
51
51
 
52
- const nextCapabilities = (state.metadata?.own_capabilities || []).filter(
52
+ const nextCapabilities = state.ownCapabilities.filter(
53
53
  (capability) => update[capability] !== false,
54
54
  );
55
55
  Object.entries(update).forEach(([capability, value]) => {
@@ -58,12 +58,7 @@ export const watchCallGrantsUpdated = (state: CallState) => {
58
58
  }
59
59
  });
60
60
 
61
- state.setMetadata((metadata) => {
62
- return {
63
- ...metadata!,
64
- own_capabilities: nextCapabilities,
65
- };
66
- });
61
+ state.setOwnCapabilities(nextCapabilities);
67
62
  }
68
63
  };
69
64
  };
@@ -9,12 +9,7 @@ import { StreamVideoEvent } from '../coordinator/connection/types';
9
9
  export const watchCallSessionStarted = (state: CallState) => {
10
10
  return function onCallSessionStarted(event: StreamVideoEvent) {
11
11
  if (event.type !== 'call.session_started') return;
12
- const { call } = event;
13
- state.setMetadata((metadata) => ({
14
- ...call,
15
- // FIXME OL: temporary, until the backend sends the own_capabilities
16
- own_capabilities: metadata?.own_capabilities || [],
17
- }));
12
+ state.setMetadata(event.call);
18
13
  };
19
14
  };
20
15
 
@@ -26,12 +21,7 @@ export const watchCallSessionStarted = (state: CallState) => {
26
21
  export const watchCallSessionEnded = (state: CallState) => {
27
22
  return function onCallSessionEnded(event: StreamVideoEvent) {
28
23
  if (event.type !== 'call.session_ended') return;
29
- const { call } = event;
30
- state.setMetadata((metadata) => ({
31
- ...call,
32
- // FIXME OL: temporary, until the backend sends the own_capabilities
33
- own_capabilities: metadata?.own_capabilities || [],
34
- }));
24
+ state.setMetadata(event.call);
35
25
  };
36
26
  };
37
27
 
@@ -1028,12 +1028,6 @@ export interface CallResponse {
1028
1028
  * @memberof CallResponse
1029
1029
  */
1030
1030
  ingress: CallIngressResponse;
1031
- /**
1032
- * The capabilities of the current user
1033
- * @type {Array<OwnCapability>}
1034
- * @memberof CallResponse
1035
- */
1036
- own_capabilities: Array<OwnCapability>;
1037
1031
  /**
1038
1032
  *
1039
1033
  * @type {boolean}
@@ -1475,6 +1469,12 @@ export interface CallStateResponseFields {
1475
1469
  * @memberof CallStateResponseFields
1476
1470
  */
1477
1471
  membership?: MemberResponse;
1472
+ /**
1473
+ *
1474
+ * @type {Array<OwnCapability>}
1475
+ * @memberof CallStateResponseFields
1476
+ */
1477
+ own_capabilities: Array<OwnCapability>;
1478
1478
  }
1479
1479
  /**
1480
1480
  *
@@ -2048,62 +2048,6 @@ export interface GeofenceSettingsRequest {
2048
2048
  */
2049
2049
  names?: Array<string>;
2050
2050
  }
2051
- /**
2052
- *
2053
- * @export
2054
- * @interface GetCallEdgeServerRequest
2055
- */
2056
- export interface GetCallEdgeServerRequest {
2057
- /**
2058
- *
2059
- * @type {{ [key: string]: Array<number>; }}
2060
- * @memberof GetCallEdgeServerRequest
2061
- */
2062
- latency_measurements: { [key: string]: Array<number> };
2063
- }
2064
- /**
2065
- *
2066
- * @export
2067
- * @interface GetCallEdgeServerResponse
2068
- */
2069
- export interface GetCallEdgeServerResponse {
2070
- /**
2071
- *
2072
- * @type {Array<UserResponse>}
2073
- * @memberof GetCallEdgeServerResponse
2074
- */
2075
- blocked_users: Array<UserResponse>;
2076
- /**
2077
- *
2078
- * @type {CallResponse}
2079
- * @memberof GetCallEdgeServerResponse
2080
- */
2081
- call: CallResponse;
2082
- /**
2083
- *
2084
- * @type {Credentials}
2085
- * @memberof GetCallEdgeServerResponse
2086
- */
2087
- credentials: Credentials;
2088
- /**
2089
- * Duration of the request in human-readable format
2090
- * @type {string}
2091
- * @memberof GetCallEdgeServerResponse
2092
- */
2093
- duration: string;
2094
- /**
2095
- *
2096
- * @type {Array<MemberResponse>}
2097
- * @memberof GetCallEdgeServerResponse
2098
- */
2099
- members: Array<MemberResponse>;
2100
- /**
2101
- *
2102
- * @type {MemberResponse}
2103
- * @memberof GetCallEdgeServerResponse
2104
- */
2105
- membership?: MemberResponse;
2106
- }
2107
2051
  /**
2108
2052
  *
2109
2053
  * @export
@@ -2140,6 +2084,12 @@ export interface GetCallResponse {
2140
2084
  * @memberof GetCallResponse
2141
2085
  */
2142
2086
  membership?: MemberResponse;
2087
+ /**
2088
+ *
2089
+ * @type {Array<OwnCapability>}
2090
+ * @memberof GetCallResponse
2091
+ */
2092
+ own_capabilities: Array<OwnCapability>;
2143
2093
  }
2144
2094
  /**
2145
2095
  *
@@ -2282,6 +2232,12 @@ export interface GetOrCreateCallResponse {
2282
2232
  * @memberof GetOrCreateCallResponse
2283
2233
  */
2284
2234
  membership?: MemberResponse;
2235
+ /**
2236
+ *
2237
+ * @type {Array<OwnCapability>}
2238
+ * @memberof GetOrCreateCallResponse
2239
+ */
2240
+ own_capabilities: Array<OwnCapability>;
2285
2241
  }
2286
2242
  /**
2287
2243
  *
@@ -2468,6 +2424,12 @@ export interface JoinCallResponse {
2468
2424
  * @memberof JoinCallResponse
2469
2425
  */
2470
2426
  membership?: MemberResponse;
2427
+ /**
2428
+ *
2429
+ * @type {Array<OwnCapability>}
2430
+ * @memberof JoinCallResponse
2431
+ */
2432
+ own_capabilities: Array<OwnCapability>;
2471
2433
  }
2472
2434
  /**
2473
2435
  *
@@ -3745,6 +3707,12 @@ export interface UpdateCallRequest {
3745
3707
  * @interface UpdateCallResponse
3746
3708
  */
3747
3709
  export interface UpdateCallResponse {
3710
+ /**
3711
+ *
3712
+ * @type {Array<UserResponse>}
3713
+ * @memberof UpdateCallResponse
3714
+ */
3715
+ blocked_users: Array<UserResponse>;
3748
3716
  /**
3749
3717
  *
3750
3718
  * @type {CallResponse}
@@ -3757,6 +3725,24 @@ export interface UpdateCallResponse {
3757
3725
  * @memberof UpdateCallResponse
3758
3726
  */
3759
3727
  duration: string;
3728
+ /**
3729
+ *
3730
+ * @type {Array<MemberResponse>}
3731
+ * @memberof UpdateCallResponse
3732
+ */
3733
+ members: Array<MemberResponse>;
3734
+ /**
3735
+ *
3736
+ * @type {MemberResponse}
3737
+ * @memberof UpdateCallResponse
3738
+ */
3739
+ membership?: MemberResponse;
3740
+ /**
3741
+ *
3742
+ * @type {Array<OwnCapability>}
3743
+ * @memberof UpdateCallResponse
3744
+ */
3745
+ own_capabilities: Array<OwnCapability>;
3760
3746
  }
3761
3747
  /**
3762
3748
  *
@@ -69,7 +69,7 @@ const getMediaSection = (sdp: string, mediaType: 'video' | 'audio') => {
69
69
  sdp.split(/(\r\n|\r|\n)/).forEach((line) => {
70
70
  const isValidLine = /^([a-z])=(.*)/.test(line);
71
71
  if (!isValidLine) return;
72
- /*
72
+ /*
73
73
  NOTE: according to https://www.rfc-editor.org/rfc/rfc8866.pdf
74
74
  Each media description starts with an "m=" line and continues to the next media description or the end of the whole session description, whichever comes first
75
75
  */
@@ -170,7 +170,7 @@ export const removeCodec = (
170
170
  sdp: string,
171
171
  mediaType: 'video' | 'audio',
172
172
  codecToRemove: string,
173
- ) => {
173
+ ): string => {
174
174
  const section = getMediaSection(sdp, mediaType);
175
175
  const mediaSection = section?.media;
176
176
  if (!mediaSection) {
@@ -190,30 +190,26 @@ export const removeCodec = (
190
190
  mediaSection.original,
191
191
  `${mediaSection.mediaWithPorts} ${newCodecOrder}`,
192
192
  )
193
- .replace(new RegExp(`${rtpMap.original}[\r\n|\r|\n]`), '') // remove the corresponding rtpmap line
194
- .replace(
195
- fmtp?.original ? new RegExp(`${fmtp?.original}[\r\n|\r|\n]`) : '',
196
- '',
197
- ); // remove the corresponding fmtp line
193
+ .replace(new RegExp(`${rtpMap.original}[\r\n]+`), '') // remove the corresponding rtpmap line
194
+ .replace(fmtp?.original ? new RegExp(`${fmtp?.original}[\r\n]+`) : '', ''); // remove the corresponding fmtp line
198
195
  };
199
196
 
200
197
  /**
201
198
  * Gets the fmtp line corresponding to opus
202
199
  */
203
- const getOpusFmtp = (sdp: string) => {
200
+ const getOpusFmtp = (sdp: string): Fmtp | undefined => {
204
201
  const section = getMediaSection(sdp, 'audio');
205
202
  const rtpMap = section?.rtpMap.find((r) => r.codec.toLowerCase() === 'opus');
206
203
  const codecId = rtpMap?.payload;
207
204
  if (codecId) {
208
- const fmtp = section?.fmtp.find((f) => f.payload === codecId);
209
- return fmtp;
205
+ return section?.fmtp.find((f) => f.payload === codecId);
210
206
  }
211
207
  };
212
208
 
213
209
  /**
214
210
  * Returns an SDP with DTX enabled or disabled.
215
211
  */
216
- export const toggleDtx = (sdp: string, enable: boolean) => {
212
+ export const toggleDtx = (sdp: string, enable: boolean): string => {
217
213
  const opusFmtp = getOpusFmtp(sdp);
218
214
  if (opusFmtp) {
219
215
  const matchDtx = /usedtx=(\d)/.exec(opusFmtp.config);
@@ -27,6 +27,17 @@ export type SoundDetectorOptions = {
27
27
  destroyStreamOnStop?: boolean;
28
28
  };
29
29
 
30
+ export type SoundDetectorState = {
31
+ isSoundDetected: boolean;
32
+ /**
33
+ * Represented as percentage (0-100) where 100% is defined by `audioLevelThreshold` property.
34
+ * Decrease time between samples (to 50-100ms) with `detectionFrequencyInMs` property.
35
+ */
36
+ audioLevel: number;
37
+ };
38
+
39
+ export type SoundStateChangeHandler = (state: SoundDetectorState) => void;
40
+
30
41
  const DETECTION_FREQUENCY_IN_MS = 500;
31
42
  const AUDIO_LEVEL_THRESHOLD = 150;
32
43
  const FFT_SIZE = 128;
@@ -41,14 +52,7 @@ const FFT_SIZE = 128;
41
52
  */
42
53
  export const createSoundDetector = (
43
54
  audioStream: MediaStream,
44
- onSoundDetectedStateChanged: (
45
- isSoundDetected: boolean,
46
- /**
47
- * Represented as percentage (0-100) where 100% is defined by `audioLevelThreshold` property.
48
- * Decrease time between samples (to 50-100ms) with `detectionFrequencyInMs` property.
49
- */
50
- audioLevel: number,
51
- ) => void,
55
+ onSoundDetectedStateChanged: SoundStateChangeHandler,
52
56
  options: SoundDetectorOptions = {},
53
57
  ) => {
54
58
  const {
@@ -78,7 +82,7 @@ export const createSoundDetector = (
78
82
  ? 100
79
83
  : Math.round((averagedDataValue / audioLevelThreshold) * 100);
80
84
 
81
- onSoundDetectedStateChanged(isSoundDetected, percentage);
85
+ onSoundDetectedStateChanged({ isSoundDetected, audioLevel: percentage });
82
86
  }, detectionFrequencyInMs);
83
87
 
84
88
  return async function stop() {
@@ -35,12 +35,12 @@ describe('Publisher', () => {
35
35
 
36
36
  beforeEach(() => {
37
37
  const dispatcher = new Dispatcher();
38
- sfuClient = new StreamSfuClient(
38
+ sfuClient = new StreamSfuClient({
39
39
  dispatcher,
40
- 'https://getstream.io/',
41
- 'https://getstream.io/ws',
42
- 'token',
43
- );
40
+ url: 'https://getstream.io/',
41
+ wsEndpoint: 'https://getstream.io/ws',
42
+ token: 'token',
43
+ });
44
44
 
45
45
  // @ts-ignore
46
46
  sfuClient['sessionId'] = sessionId;
@@ -24,13 +24,14 @@ export const join = async (
24
24
  await httpClient.connectionIdPromise;
25
25
 
26
26
  const joinCallResponse = await doJoin(httpClient, type, id, data);
27
- const { call, credentials, members } = joinCallResponse;
27
+ const { call, credentials, members, own_capabilities } = joinCallResponse;
28
28
  return {
29
29
  connectionConfig: toRtcConfiguration(credentials.ice_servers),
30
30
  sfuServer: credentials.server,
31
31
  token: credentials.token,
32
32
  metadata: call,
33
33
  members,
34
+ ownCapabilities: own_capabilities,
34
35
  };
35
36
  };
36
37
 
@@ -68,15 +69,20 @@ const doJoin = async (
68
69
 
69
70
  const getLocationHint = async () => {
70
71
  const hintURL = `https://hint.stream-io-video.com/`;
72
+ const abortController = new AbortController();
73
+ const timeoutId = setTimeout(() => abortController.abort(), 1000);
71
74
  try {
72
75
  const response = await fetch(hintURL, {
73
76
  method: 'HEAD',
77
+ signal: abortController.signal,
74
78
  });
75
79
  const awsPop = response.headers.get('x-amz-cf-pop') || 'ERR';
76
80
  return awsPop.substring(0, 3); // AMS1-P2 -> AMS
77
81
  } catch (e) {
78
82
  console.error(`Failed to get location hint from ${hintURL}`, e);
79
83
  return 'ERR';
84
+ } finally {
85
+ clearTimeout(timeoutId);
80
86
  }
81
87
  };
82
88
 
@@ -269,19 +269,14 @@ export class Publisher {
269
269
  };
270
270
 
271
271
  updateVideoPublishQuality = async (enabledRids: string[]) => {
272
- console.log(
273
- 'Updating publish quality, qualities requested by SFU:',
274
- enabledRids,
275
- );
272
+ console.log('Update publish quality, requested rids by SFU:', enabledRids);
276
273
 
277
274
  const videoSender = this.transceiverRegistry[TrackType.VIDEO]?.sender;
278
-
279
275
  if (!videoSender) return;
280
276
 
281
277
  const params = videoSender.getParameters();
282
278
  let changed = false;
283
279
  params.encodings.forEach((enc) => {
284
- console.log(enc.rid, enc.active);
285
280
  // flip 'active' flag only when necessary
286
281
  const shouldEnable = enabledRids.includes(enc.rid!);
287
282
  if (shouldEnable !== enc.active) {
@@ -294,6 +289,12 @@ export class Publisher {
294
289
  console.warn('No suitable video encoding quality found');
295
290
  }
296
291
  await videoSender.setParameters(params);
292
+ console.log(
293
+ `Update publish quality, enabled rids: ${params.encodings
294
+ .filter((e) => e.active)
295
+ .map((e) => e.rid)
296
+ .join(', ')}`,
297
+ );
297
298
  }
298
299
  };
299
300
 
@@ -341,7 +342,7 @@ export class Publisher {
341
342
  const offer = await this.publisher.createOffer();
342
343
  let sdp = offer.sdp;
343
344
  if (sdp) {
344
- toggleDtx(sdp, this.isDtxEnabled);
345
+ sdp = toggleDtx(sdp, this.isDtxEnabled);
345
346
  if (isReactNative()) {
346
347
  if (this.preferredVideoCodec) {
347
348
  sdp = setPreferredCodec(sdp, 'video', this.preferredVideoCodec);
@@ -25,7 +25,10 @@ export const findOptimalVideoLayers = (
25
25
  ) => {
26
26
  const optimalVideoLayers: OptimalVideoLayer[] = [];
27
27
  const settings = videoTrack.getSettings();
28
- const { width: w = 0, height: h = 0 } = settings;
28
+ const {
29
+ width: w = targetResolution.width,
30
+ height: h = targetResolution.height,
31
+ } = settings;
29
32
 
30
33
  const maxBitrate = getComputedMaxBitrate(targetResolution, w, h);
31
34
  let downscaleFactor = 1;
@@ -14,6 +14,7 @@ import {
14
14
  CallRecording,
15
15
  CallResponse,
16
16
  MemberResponse,
17
+ OwnCapability,
17
18
  PermissionRequestEvent,
18
19
  } from '../gen/coordinator';
19
20
  import { TrackType } from '../gen/video/sfu/models/models';
@@ -91,6 +92,13 @@ export class CallState {
91
92
  */
92
93
  private membersSubject = new BehaviorSubject<MemberResponse[]>([]);
93
94
 
95
+ /**
96
+ * The list of capabilities of the current user.
97
+ *
98
+ * @private
99
+ */
100
+ private ownCapabilitiesSubject = new BehaviorSubject<OwnCapability[]>([]);
101
+
94
102
  /**
95
103
  * The calling state.
96
104
  *
@@ -249,6 +257,11 @@ export class CallState {
249
257
  */
250
258
  members$: Observable<MemberResponse[]>;
251
259
 
260
+ /**
261
+ * The list of capabilities of the current user.
262
+ */
263
+ ownCapabilities$: Observable<OwnCapability[]>;
264
+
252
265
  /**
253
266
  * The calling state.
254
267
  */
@@ -307,6 +320,7 @@ export class CallState {
307
320
  this.callRecordingList$ = this.callRecordingListSubject.asObservable();
308
321
  this.metadata$ = this.metadataSubject.asObservable();
309
322
  this.members$ = this.membersSubject.asObservable();
323
+ this.ownCapabilities$ = this.ownCapabilitiesSubject.asObservable();
310
324
  this.callingState$ = this.callingStateSubject.asObservable();
311
325
  }
312
326
 
@@ -555,6 +569,23 @@ export class CallState {
555
569
  this.setCurrentValue(this.membersSubject, members);
556
570
  };
557
571
 
572
+ /**
573
+ * The capabilities of the current user for the current call.
574
+ */
575
+ get ownCapabilities() {
576
+ return this.getCurrentValue(this.ownCapabilities$);
577
+ }
578
+
579
+ /**
580
+ * Sets the own capabilities.
581
+ *
582
+ * @internal
583
+ * @param capabilities the capabilities to set.
584
+ */
585
+ setOwnCapabilities = (capabilities: Patch<OwnCapability[]>) => {
586
+ return this.setCurrentValue(this.ownCapabilitiesSubject, capabilities);
587
+ };
588
+
558
589
  /**
559
590
  * Will try to find the participant with the given sessionId in the current call.
560
591
  *
package/src/types.ts CHANGED
@@ -6,6 +6,7 @@ import type {
6
6
  CallResponse,
7
7
  JoinCallRequest,
8
8
  MemberResponse,
9
+ OwnCapability,
9
10
  ReactionResponse,
10
11
  } from './gen/coordinator';
11
12
  import type { StreamClient } from './coordinator/connection/client';
@@ -184,6 +185,13 @@ export type CallConstructor = {
184
185
  */
185
186
  members?: MemberResponse[];
186
187
 
188
+ /**
189
+ * An optional list of {@link OwnCapability} coming from the backed.
190
+ * If provided, the call will be initialized with the data from this object.
191
+ * This is useful when initializing a new "pending call" from an event.
192
+ */
193
+ ownCapabilities?: OwnCapability[];
194
+
187
195
  /**
188
196
  * Flags the call as a ringing call.
189
197
  * @default false