@stream-io/video-client 1.18.9 → 1.19.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +16 -7
  2. package/dist/index.browser.es.js +2612 -2301
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +2612 -2300
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.es.js +2612 -2301
  8. package/dist/index.es.js.map +1 -1
  9. package/dist/src/Call.d.ts +3 -0
  10. package/dist/src/StreamSfuClient.d.ts +9 -2
  11. package/dist/src/gen/coordinator/index.d.ts +266 -5
  12. package/dist/src/gen/google/protobuf/struct.d.ts +1 -3
  13. package/dist/src/gen/google/protobuf/timestamp.d.ts +1 -3
  14. package/dist/src/gen/video/sfu/event/events.d.ts +6 -0
  15. package/dist/src/gen/video/sfu/models/models.d.ts +46 -0
  16. package/dist/src/gen/video/sfu/signal_rpc/signal.d.ts +13 -1
  17. package/dist/src/rpc/createClient.d.ts +2 -0
  18. package/dist/src/rtc/BasePeerConnection.d.ts +10 -3
  19. package/dist/src/stats/index.d.ts +2 -0
  20. package/dist/src/stats/rtc/Tracer.d.ts +15 -0
  21. package/dist/src/stats/rtc/index.d.ts +2 -0
  22. package/dist/src/stats/rtc/mediaDevices.d.ts +2 -0
  23. package/dist/src/stats/rtc/pc.d.ts +2 -0
  24. package/dist/src/stats/rtc/types.d.ts +8 -0
  25. package/index.ts +4 -0
  26. package/package.json +1 -1
  27. package/src/Call.ts +54 -29
  28. package/src/StreamSfuClient.ts +22 -9
  29. package/src/devices/MicrophoneManager.ts +5 -2
  30. package/src/devices/__tests__/MicrophoneManager.test.ts +9 -6
  31. package/src/devices/__tests__/MicrophoneManagerRN.test.ts +9 -6
  32. package/src/gen/coordinator/index.ts +262 -5
  33. package/src/gen/google/protobuf/struct.ts +13 -8
  34. package/src/gen/google/protobuf/timestamp.ts +10 -8
  35. package/src/gen/video/sfu/event/events.ts +8 -1
  36. package/src/gen/video/sfu/models/models.ts +63 -1
  37. package/src/gen/video/sfu/signal_rpc/signal.client.ts +1 -1
  38. package/src/gen/video/sfu/signal_rpc/signal.ts +27 -1
  39. package/src/rpc/__tests__/createClient.test.ts +38 -0
  40. package/src/rpc/createClient.ts +30 -0
  41. package/src/rtc/BasePeerConnection.ts +22 -4
  42. package/src/rtc/Publisher.ts +3 -2
  43. package/src/rtc/Subscriber.ts +0 -2
  44. package/src/rtc/__tests__/Subscriber.test.ts +1 -0
  45. package/src/rtc/helpers/__tests__/rtcConfiguration.test.ts +1 -0
  46. package/src/rtc/helpers/rtcConfiguration.ts +1 -0
  47. package/src/stats/SfuStatsReporter.ts +36 -12
  48. package/src/stats/index.ts +2 -0
  49. package/src/stats/rtc/Tracer.ts +42 -0
  50. package/src/stats/rtc/index.ts +5 -0
  51. package/src/stats/rtc/mediaDevices.ts +42 -0
  52. package/src/stats/rtc/pc.ts +130 -0
  53. package/src/stats/rtc/types.ts +26 -0
  54. package/src/store/CallState.ts +10 -0
@@ -9,14 +9,16 @@ import { PeerType } from '../gen/video/sfu/models/models';
9
9
  import { StreamSfuClient } from '../StreamSfuClient';
10
10
  import { AllSfuEvents, Dispatcher } from './Dispatcher';
11
11
  import { withoutConcurrency } from '../helpers/concurrency';
12
+ import { Tracer, traceRTCPeerConnection, TraceSlice } from '../stats';
12
13
 
13
14
  export type BasePeerConnectionOpts = {
14
15
  sfuClient: StreamSfuClient;
15
16
  state: CallState;
16
17
  connectionConfig?: RTCConfiguration;
17
18
  dispatcher: Dispatcher;
18
- onUnrecoverableError?: () => void;
19
+ onUnrecoverableError?: (reason: string) => void;
19
20
  logTag: string;
21
+ enableTracing: boolean;
20
22
  };
21
23
 
22
24
  /**
@@ -31,10 +33,11 @@ export abstract class BasePeerConnection {
31
33
  protected readonly dispatcher: Dispatcher;
32
34
  protected sfuClient: StreamSfuClient;
33
35
 
34
- protected onUnrecoverableError?: () => void;
36
+ protected onUnrecoverableError?: (reason: string) => void;
35
37
  protected isIceRestarting = false;
36
38
  private isDisposed = false;
37
39
 
40
+ private readonly tracer?: Tracer;
38
41
  private readonly subscriptions: (() => void)[] = [];
39
42
  private unsubscribeIceTrickle?: () => void;
40
43
 
@@ -50,6 +53,7 @@ export abstract class BasePeerConnection {
50
53
  dispatcher,
51
54
  onUnrecoverableError,
52
55
  logTag,
56
+ enableTracing,
53
57
  }: BasePeerConnectionOpts,
54
58
  ) {
55
59
  this.peerType = peerType;
@@ -62,6 +66,11 @@ export abstract class BasePeerConnection {
62
66
  logTag,
63
67
  ]);
64
68
  this.pc = new RTCPeerConnection(connectionConfig);
69
+ if (enableTracing) {
70
+ this.tracer = new Tracer(logTag);
71
+ this.tracer.trace('create', connectionConfig);
72
+ traceRTCPeerConnection(this.pc, this.tracer.trace);
73
+ }
65
74
  this.pc.addEventListener('icecandidate', this.onIceCandidate);
66
75
  this.pc.addEventListener('icecandidateerror', this.onIceCandidateError);
67
76
  this.pc.addEventListener(
@@ -80,6 +89,7 @@ export abstract class BasePeerConnection {
80
89
  this.isDisposed = true;
81
90
  this.detachEventHandlers();
82
91
  this.pc.close();
92
+ this.tracer?.dispose();
83
93
  }
84
94
 
85
95
  /**
@@ -163,6 +173,13 @@ export abstract class BasePeerConnection {
163
173
  return this.pc.getStats(selector);
164
174
  };
165
175
 
176
+ /**
177
+ * Returns the current tracing buffer.
178
+ */
179
+ getTrace = (): TraceSlice | undefined => {
180
+ return this.tracer?.take();
181
+ };
182
+
166
183
  /**
167
184
  * Handles the ICECandidate event and
168
185
  * Initiates an ICE Trickle process with the SFU.
@@ -214,8 +231,9 @@ export abstract class BasePeerConnection {
214
231
  this.logger('debug', `Attempting to restart ICE`);
215
232
  this.restartIce().catch((e) => {
216
233
  if (this.isDisposed) return;
217
- this.logger('error', `ICE restart failed`, e);
218
- this.onUnrecoverableError?.();
234
+ const reason = `ICE restart failed`;
235
+ this.logger('error', reason, e);
236
+ this.onUnrecoverableError?.(`${reason}: ${e}`);
219
237
  });
220
238
  }
221
239
  };
@@ -45,8 +45,9 @@ export class Publisher extends BasePeerConnection {
45
45
  this.on('iceRestart', (iceRestart) => {
46
46
  if (iceRestart.peerType !== PeerType.PUBLISHER_UNSPECIFIED) return;
47
47
  this.restartIce().catch((err) => {
48
- this.logger('warn', `ICERestart failed`, err);
49
- this.onUnrecoverableError?.();
48
+ const reason = `ICE restart failed`;
49
+ this.logger('warn', reason, err);
50
+ this.onUnrecoverableError?.(`${reason}: ${err}`);
50
51
  });
51
52
  });
52
53
 
@@ -144,8 +144,6 @@ export class Subscriber extends BasePeerConnection {
144
144
  };
145
145
 
146
146
  private negotiate = async (subscriberOffer: SubscriberOffer) => {
147
- this.logger('info', `Received subscriberOffer`, subscriberOffer);
148
-
149
147
  await this.pc.setRemoteDescription({
150
148
  type: 'offer',
151
149
  sdp: subscriberOffer.sdp,
@@ -190,6 +190,7 @@ describe('Subscriber', () => {
190
190
  subscriber['pc'].createAnswer = vi
191
191
  .fn()
192
192
  .mockResolvedValue({ sdp: 'answer-sdp' });
193
+ vi.spyOn(subscriber['pc'], 'setRemoteDescription').mockResolvedValue();
193
194
 
194
195
  const offer = SubscriberOffer.create({ sdp: 'offer-sdp' });
195
196
  // @ts-expect-error - private method
@@ -17,6 +17,7 @@ describe('rtcConfiguration', () => {
17
17
  },
18
18
  ];
19
19
  expect(toRtcConfiguration(config)).toEqual({
20
+ bundlePolicy: 'max-bundle',
20
21
  iceServers: [
21
22
  {
22
23
  urls: ['stun:stun.l.google.com:19302'],
@@ -2,6 +2,7 @@ import { ICEServer } from '../../gen/coordinator';
2
2
 
3
3
  export const toRtcConfiguration = (config: ICEServer[]): RTCConfiguration => {
4
4
  return {
5
+ bundlePolicy: 'max-bundle',
5
6
  iceServers: config.map((ice) => ({
6
7
  urls: ice.urls,
7
8
  username: ice.username,
@@ -3,6 +3,7 @@ import { StreamSfuClient } from '../StreamSfuClient';
3
3
  import { OwnCapability, StatsOptions } from '../gen/coordinator';
4
4
  import { getLogger } from '../logger';
5
5
  import { Publisher, Subscriber } from '../rtc';
6
+ import { tracer as mediaStatsTracer } from './rtc/mediaDevices';
6
7
  import { flatten, getSdkName, getSdkVersion } from './utils';
7
8
  import { getDeviceState, getWebRTCInfo } from '../helpers/client-details';
8
9
  import {
@@ -147,23 +148,46 @@ export class SfuStatsReporter {
147
148
  });
148
149
  };
149
150
 
150
- private run = async (telemetryData?: Telemetry) => {
151
+ private run = async (telemetry?: Telemetry) => {
151
152
  const [subscriberStats, publisherStats] = await Promise.all([
152
153
  this.subscriber.getStats().then(flatten).then(JSON.stringify),
153
154
  this.publisher?.getStats().then(flatten).then(JSON.stringify) ?? '[]',
154
155
  ]);
155
156
 
156
- await this.sfuClient.sendStats({
157
- sdk: this.sdkName,
158
- sdkVersion: this.sdkVersion,
159
- webrtcVersion: this.webRTCVersion,
160
- subscriberStats,
161
- publisherStats,
162
- audioDevices: this.inputDevices.get('mic'),
163
- videoDevices: this.inputDevices.get('camera'),
164
- deviceState: getDeviceState(),
165
- telemetry: telemetryData,
166
- });
157
+ const subscriberTrace = this.subscriber.getTrace();
158
+ const publisherTrace = this.publisher?.getTrace();
159
+ const mediaTrace = mediaStatsTracer.take();
160
+ const sfuTrace = this.sfuClient.getTrace();
161
+ const publisherTraces = [
162
+ ...mediaTrace.snapshot,
163
+ ...(sfuTrace?.snapshot ?? []),
164
+ ...(publisherTrace?.snapshot ?? []),
165
+ ];
166
+
167
+ try {
168
+ await this.sfuClient.sendStats({
169
+ sdk: this.sdkName,
170
+ sdkVersion: this.sdkVersion,
171
+ webrtcVersion: this.webRTCVersion,
172
+ subscriberStats,
173
+ subscriberRtcStats: subscriberTrace
174
+ ? JSON.stringify(subscriberTrace.snapshot)
175
+ : '',
176
+ publisherStats,
177
+ publisherRtcStats:
178
+ publisherTraces.length > 0 ? JSON.stringify(publisherTraces) : '',
179
+ audioDevices: this.inputDevices.get('mic'),
180
+ videoDevices: this.inputDevices.get('camera'),
181
+ deviceState: getDeviceState(),
182
+ telemetry,
183
+ });
184
+ } catch (err) {
185
+ publisherTrace?.rollback();
186
+ subscriberTrace?.rollback();
187
+ mediaTrace.rollback();
188
+ sfuTrace?.rollback();
189
+ throw err;
190
+ }
167
191
  };
168
192
 
169
193
  start = () => {
@@ -1,3 +1,5 @@
1
1
  export * from './CallStateStatsReporter';
2
2
  export * from './SfuStatsReporter';
3
3
  export * from './types';
4
+ export * from './utils';
5
+ export * from './rtc';
@@ -0,0 +1,42 @@
1
+ import type { Trace, TraceRecord } from './types';
2
+
3
+ export type TraceSlice = {
4
+ snapshot: TraceRecord[];
5
+ rollback: () => void;
6
+ };
7
+
8
+ export class Tracer {
9
+ private buffer: TraceRecord[] = [];
10
+ private enabled = true;
11
+ private readonly id: string | null;
12
+
13
+ constructor(id: string | null) {
14
+ this.id = id;
15
+ }
16
+
17
+ setEnabled = (enabled: boolean) => {
18
+ if (this.enabled === enabled) return;
19
+ this.enabled = enabled;
20
+ this.buffer = [];
21
+ };
22
+
23
+ trace: Trace = (tag, data) => {
24
+ if (!this.enabled) return;
25
+ this.buffer.push([tag, this.id, data, Date.now()]);
26
+ };
27
+
28
+ take = (): TraceSlice => {
29
+ const snapshot = this.buffer;
30
+ this.buffer = [];
31
+ return {
32
+ snapshot,
33
+ rollback: () => {
34
+ this.buffer.unshift(...snapshot);
35
+ },
36
+ };
37
+ };
38
+
39
+ dispose = () => {
40
+ this.buffer = [];
41
+ };
42
+ }
@@ -0,0 +1,5 @@
1
+ // Based on the original code from:
2
+ // https://github.com/fippo/rtcstats
3
+
4
+ export * from './pc';
5
+ export * from './Tracer';
@@ -0,0 +1,42 @@
1
+ import { Tracer } from './Tracer';
2
+
3
+ export const tracer = new Tracer(null);
4
+
5
+ if (
6
+ typeof navigator !== 'undefined' &&
7
+ typeof navigator.mediaDevices !== 'undefined'
8
+ ) {
9
+ const dumpStream = (stream: MediaStream) => ({
10
+ id: stream.id,
11
+ tracks: stream.getTracks().map((track) => ({
12
+ id: track.id,
13
+ kind: track.kind,
14
+ label: track.label,
15
+ enabled: track.enabled,
16
+ muted: track.muted,
17
+ readyState: track.readyState,
18
+ })),
19
+ });
20
+
21
+ const trace = tracer.trace;
22
+ const target = navigator.mediaDevices;
23
+ for (const method of ['getUserMedia', 'getDisplayMedia'] as const) {
24
+ const original = target[method];
25
+ if (!original) continue;
26
+
27
+ target[method] = async function tracedMethod(
28
+ constraints: MediaStreamConstraints,
29
+ ) {
30
+ const tag = `navigator.mediaDevices.${method}`;
31
+ trace(tag, constraints);
32
+ try {
33
+ const stream = await original.call(target, constraints);
34
+ trace(`${tag}OnSuccess`, dumpStream(stream));
35
+ return stream;
36
+ } catch (err) {
37
+ trace(`${tag}OnFailure`, (err as Error).name);
38
+ throw err;
39
+ }
40
+ };
41
+ }
42
+ }
@@ -0,0 +1,130 @@
1
+ import type { RTCStatsDataType, Trace } from './types';
2
+
3
+ export const traceRTCPeerConnection = (pc: RTCPeerConnection, trace: Trace) => {
4
+ pc.addEventListener('icecandidate', (e) => {
5
+ trace('onicecandidate', e.candidate);
6
+ });
7
+ pc.addEventListener('track', (e) => {
8
+ const streams = e.streams.map((stream) => `stream:${stream.id}`);
9
+ trace('ontrack', `${e.track.kind}:${e.track.id} ${streams}`);
10
+ });
11
+ pc.addEventListener('signalingstatechange', () => {
12
+ trace('onsignalingstatechange', pc.signalingState);
13
+ });
14
+ pc.addEventListener('iceconnectionstatechange', () => {
15
+ trace('oniceconnectionstatechange', pc.iceConnectionState);
16
+ });
17
+ pc.addEventListener('icegatheringstatechange', () => {
18
+ trace('onicegatheringstatechange', pc.iceGatheringState);
19
+ });
20
+ pc.addEventListener('connectionstatechange', () => {
21
+ trace('onconnectionstatechange', pc.connectionState);
22
+ });
23
+ pc.addEventListener('negotiationneeded', () => {
24
+ trace('onnegotiationneeded', undefined);
25
+ });
26
+ pc.addEventListener('datachannel', ({ channel }) => {
27
+ trace('ondatachannel', [channel.id, channel.label]);
28
+ });
29
+
30
+ let prev: Record<string, RTCStats> = {};
31
+ const getStats = () => {
32
+ pc.getStats(null)
33
+ .then((stats) => {
34
+ const now = toObject(stats);
35
+ trace('getstats', deltaCompression(prev, now));
36
+ prev = now;
37
+ })
38
+ .catch((err) => {
39
+ trace('getstatsOnFailure', (err as Error).toString());
40
+ });
41
+ };
42
+
43
+ const interval = setInterval(() => {
44
+ getStats();
45
+ }, 8000);
46
+
47
+ pc.addEventListener('connectionstatechange', () => {
48
+ const state = pc.connectionState;
49
+ if (state === 'connected' || state === 'failed') {
50
+ getStats();
51
+ }
52
+ });
53
+
54
+ const origClose = pc.close;
55
+ pc.close = function tracedClose() {
56
+ clearInterval(interval);
57
+ trace('close', undefined);
58
+ return origClose.call(this);
59
+ };
60
+
61
+ for (const method of [
62
+ 'createOffer',
63
+ 'createAnswer',
64
+ 'setLocalDescription',
65
+ 'setRemoteDescription',
66
+ 'addIceCandidate',
67
+ ] as const) {
68
+ const original = pc[method];
69
+ if (!original) continue;
70
+
71
+ // @ts-expect-error we don't use deprecated APIs
72
+ pc[method] = async function tracedMethod(...args: any[]) {
73
+ try {
74
+ trace(method, args);
75
+ // @ts-expect-error improper types
76
+ const result = await original.apply(this, args);
77
+ trace(`${method}OnSuccess`, result as RTCStatsDataType);
78
+ return result;
79
+ } catch (err) {
80
+ trace(`${method}OnFailure`, (err as Error).toString());
81
+ throw err;
82
+ }
83
+ };
84
+ }
85
+ };
86
+
87
+ const toObject = (s: RTCStatsReport): Record<string, RTCStats> => {
88
+ const obj: Record<string, RTCStats> = {};
89
+ s.forEach((v, k) => {
90
+ obj[k] = v;
91
+ });
92
+ return obj;
93
+ };
94
+
95
+ /**
96
+ * Apply delta compression to the stats report.
97
+ * Reduces size by ~90%.
98
+ * To reduce further, report keys could be compressed.
99
+ */
100
+ const deltaCompression = (
101
+ oldStats: Record<any, any>,
102
+ newStats: Record<any, any>,
103
+ ): Record<any, any> => {
104
+ newStats = JSON.parse(JSON.stringify(newStats));
105
+
106
+ for (const [id, report] of Object.entries(newStats)) {
107
+ delete report.id;
108
+ if (!oldStats[id]) continue;
109
+
110
+ for (const [name, value] of Object.entries(report)) {
111
+ if (value === oldStats[id][name]) {
112
+ delete report[name];
113
+ }
114
+ }
115
+ }
116
+
117
+ let timestamp = -Infinity;
118
+ for (const report of Object.values(newStats)) {
119
+ if (report.timestamp > timestamp) {
120
+ timestamp = report.timestamp;
121
+ }
122
+ }
123
+ for (const report of Object.values(newStats)) {
124
+ if (report.timestamp === timestamp) {
125
+ report.timestamp = 0;
126
+ }
127
+ }
128
+ newStats.timestamp = timestamp;
129
+ return newStats;
130
+ };
@@ -0,0 +1,26 @@
1
+ export type RTCStatsDataType =
2
+ | RTCConfiguration
3
+ | RTCIceCandidate
4
+ | RTCSignalingState
5
+ | RTCIceConnectionState
6
+ | RTCIceGatheringState
7
+ | RTCPeerConnectionState
8
+ | [number | null | string] // RTCDataChannelEvent
9
+ | string
10
+ | RTCOfferOptions
11
+ | [string | RTCDataChannelInit | undefined] // createDataChannel
12
+ | (RTCOfferOptions | undefined) // createOffer | createAnswer
13
+ | RTCSessionDescriptionInit
14
+ | (RTCIceCandidateInit | RTCIceCandidate) // addIceCandidate
15
+ | object
16
+ | null
17
+ | undefined;
18
+
19
+ export type Trace = (tag: string, data: RTCStatsDataType) => void;
20
+
21
+ export type TraceRecord = [
22
+ tag: string,
23
+ id: string | null,
24
+ data: RTCStatsDataType,
25
+ timestamp: number,
26
+ ];
@@ -413,6 +413,7 @@ export class CallState {
413
413
 
414
414
  this.eventHandlers = {
415
415
  // these events are not updating the call state:
416
+ 'call.frame_recording_ready': undefined,
416
417
  'call.permission_request': undefined,
417
418
  'call.recording_ready': undefined,
418
419
  'call.rtmp_broadcast_failed': undefined,
@@ -445,6 +446,15 @@ export class CallState {
445
446
  this.updateFromCallResponse(e.call);
446
447
  this.setCurrentValue(this.endedBySubject, e.user);
447
448
  },
449
+ 'call.frame_recording_failed': (e) => {
450
+ this.updateFromCallResponse(e.call);
451
+ },
452
+ 'call.frame_recording_started': (e) => {
453
+ this.updateFromCallResponse(e.call);
454
+ },
455
+ 'call.frame_recording_stopped': (e) => {
456
+ this.updateFromCallResponse(e.call);
457
+ },
448
458
  'call.hls_broadcasting_failed': this.updateFromHLSBroadcastingFailed,
449
459
  'call.hls_broadcasting_started': (e) => {
450
460
  this.updateFromCallResponse(e.call);