@stream-io/video-client 1.52.1-beta.0 → 1.53.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 (69) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/index.browser.es.js +819 -123
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +819 -122
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +819 -123
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/Call.d.ts +6 -14
  9. package/dist/src/StreamVideoClient.d.ts +2 -0
  10. package/dist/src/coordinator/connection/client.d.ts +1 -0
  11. package/dist/src/devices/MicrophoneManager.d.ts +6 -0
  12. package/dist/src/errors/SfuTimeoutError.d.ts +8 -0
  13. package/dist/src/errors/index.d.ts +1 -0
  14. package/dist/src/gen/google/protobuf/struct.d.ts +1 -3
  15. package/dist/src/gen/google/protobuf/timestamp.d.ts +1 -3
  16. package/dist/src/gen/video/sfu/event/events.d.ts +1 -22
  17. package/dist/src/gen/video/sfu/models/models.d.ts +0 -4
  18. package/dist/src/gen/video/sfu/signal_rpc/signal.client.d.ts +2 -23
  19. package/dist/src/helpers/firstVideoFrame.d.ts +7 -0
  20. package/dist/src/reporting/ClientEventReporter.d.ts +84 -0
  21. package/dist/src/reporting/index.d.ts +1 -0
  22. package/dist/src/rtc/BasePeerConnection.d.ts +5 -2
  23. package/dist/src/rtc/Publisher.d.ts +1 -4
  24. package/dist/src/rtc/Subscriber.d.ts +0 -7
  25. package/dist/src/rtc/types.d.ts +24 -1
  26. package/dist/src/types.d.ts +16 -0
  27. package/package.json +1 -1
  28. package/src/Call.ts +185 -106
  29. package/src/StreamSfuClient.ts +3 -3
  30. package/src/StreamVideoClient.ts +18 -3
  31. package/src/__tests__/Call.autodrop.test.ts +4 -1
  32. package/src/__tests__/Call.lifecycle.test.ts +4 -1
  33. package/src/__tests__/Call.publishing.test.ts +4 -1
  34. package/src/__tests__/Call.test.ts +23 -0
  35. package/src/coordinator/connection/client.ts +5 -0
  36. package/src/devices/MicrophoneManager.ts +16 -0
  37. package/src/devices/__tests__/CameraManager.test.ts +10 -1
  38. package/src/devices/__tests__/DeviceManager.test.ts +10 -1
  39. package/src/devices/__tests__/DeviceManagerFilters.test.ts +4 -1
  40. package/src/devices/__tests__/MicrophoneManager.test.ts +10 -1
  41. package/src/devices/__tests__/MicrophoneManagerRN.test.ts +78 -2
  42. package/src/devices/__tests__/ScreenShareManager.test.ts +4 -1
  43. package/src/devices/__tests__/SpeakerManager.test.ts +13 -4
  44. package/src/errors/SfuTimeoutError.ts +7 -0
  45. package/src/errors/index.ts +1 -0
  46. package/src/events/__tests__/call.test.ts +2 -0
  47. package/src/events/__tests__/mutes.test.ts +4 -1
  48. package/src/events/call.ts +8 -0
  49. package/src/gen/google/protobuf/struct.ts +12 -7
  50. package/src/gen/google/protobuf/timestamp.ts +7 -6
  51. package/src/gen/video/sfu/event/events.ts +25 -23
  52. package/src/gen/video/sfu/models/models.ts +1 -11
  53. package/src/gen/video/sfu/signal_rpc/signal.client.ts +29 -25
  54. package/src/gen/video/sfu/signal_rpc/signal.ts +0 -1
  55. package/src/helpers/__tests__/AudioBindingsWatchdog.test.ts +6 -3
  56. package/src/helpers/__tests__/DynascaleManager.test.ts +6 -3
  57. package/src/helpers/__tests__/ViewportTracker.test.ts +6 -3
  58. package/src/helpers/__tests__/firstVideoFrame.test.ts +91 -0
  59. package/src/helpers/client-details.ts +1 -1
  60. package/src/helpers/firstVideoFrame.ts +38 -0
  61. package/src/reporting/ClientEventReporter.ts +864 -0
  62. package/src/reporting/__tests__/ClientEventReporter.test.ts +620 -0
  63. package/src/reporting/index.ts +1 -0
  64. package/src/rtc/BasePeerConnection.ts +30 -0
  65. package/src/rtc/Publisher.ts +0 -4
  66. package/src/rtc/Subscriber.ts +2 -28
  67. package/src/rtc/__tests__/Call.reconnect.test.ts +3 -0
  68. package/src/rtc/types.ts +34 -0
  69. package/src/types.ts +18 -0
@@ -39,7 +39,6 @@ export class Publisher extends BasePeerConnection {
39
39
  private readonly transceiverCache = new TransceiverCache();
40
40
  private readonly clonedTracks = new Set<MediaStreamTrack>();
41
41
  private publishOptions: PublishOption[];
42
- private readonly selfSubEnabled: boolean;
43
42
 
44
43
  /**
45
44
  * Constructs a new `Publisher` instance.
@@ -47,11 +46,9 @@ export class Publisher extends BasePeerConnection {
47
46
  constructor(
48
47
  baseOptions: BasePeerConnectionOpts,
49
48
  publishOptions: PublishOption[],
50
- opts: { selfSubEnabled?: boolean } = {},
51
49
  ) {
52
50
  super(PeerType.PUBLISHER_UNSPECIFIED, baseOptions);
53
51
  this.publishOptions = publishOptions;
54
- this.selfSubEnabled = opts.selfSubEnabled ?? false;
55
52
 
56
53
  this.on('iceRestart', (iceRestart) => {
57
54
  if (iceRestart.peerType !== PeerType.PUBLISHER_UNSPECIFIED) return;
@@ -579,7 +576,6 @@ export class Publisher extends BasePeerConnection {
579
576
  muted: !isTrackLive,
580
577
  codec: publishOption.codec,
581
578
  publishOptionId: publishOption.id,
582
- selfSubAudioVideo: this.selfSubEnabled,
583
579
  };
584
580
  };
585
581
 
@@ -14,14 +14,6 @@ import { enableStereo, removeCodecsExcept } from './helpers/sdp';
14
14
  * @internal
15
15
  */
16
16
  export class Subscriber extends BasePeerConnection {
17
- /**
18
- * Remote streams received from the SFU. For a self-sub case
19
- * we need to be able to distinguish between the local capture stream.
20
- * The map will never contain local streams so we can safely use it to
21
- * check if the stream is remote and dispose it when needed.
22
- */
23
- private trackedStreams: WeakSet<MediaStream> = new WeakSet();
24
-
25
17
  /**
26
18
  * Constructs a new `Subscriber` instance.
27
19
  */
@@ -83,7 +75,6 @@ export class Subscriber extends BasePeerConnection {
83
75
  const participantToUpdate = this.state.participants.find(
84
76
  (p) => p.trackLookupPrefix === trackId,
85
77
  );
86
- const isSelfSub = !!participantToUpdate?.isLocalParticipant;
87
78
  this.logger.debug(
88
79
  `[onTrack]: Got remote ${rawTrackType} track for userId: ${participantToUpdate?.userId}`,
89
80
  track.id,
@@ -103,6 +94,7 @@ export class Subscriber extends BasePeerConnection {
103
94
  track.addEventListener('unmute', () => {
104
95
  this.logger.info(`[onTrack]: Track unmuted: ${trackDebugInfo}`);
105
96
  this.setRemoteTrackInterrupted(trackId, trackType, false);
97
+ this.onRemoteTrackUnmute?.(trackType, track.id);
106
98
  });
107
99
  track.addEventListener('ended', () => {
108
100
  this.logger.info(`[onTrack]: Track ended: ${trackDebugInfo}`);
@@ -116,10 +108,6 @@ export class Subscriber extends BasePeerConnection {
116
108
 
117
109
  this.trackIdToTrackType.set(track.id, trackType);
118
110
 
119
- if (isSelfSub) {
120
- this.trackedStreams.add(primaryStream);
121
- }
122
-
123
111
  if (!participantToUpdate) {
124
112
  this.logger.warn(
125
113
  `[onTrack]: Received track for unknown participant: ${trackId}`,
@@ -140,13 +128,6 @@ export class Subscriber extends BasePeerConnection {
140
128
  return;
141
129
  }
142
130
 
143
- // Self-sub loopback audio routes to the speaker by default, which
144
- // would echo the local user's voice. Default-mute here; consumers
145
- // (the loopback recording hook) re-enable explicitly when needed.
146
- if (isSelfSub && e.track.kind === 'audio') {
147
- e.track.enabled = false;
148
- }
149
-
150
131
  // get the previous stream to dispose it later
151
132
  // usually this happens during migration, when the stream is replaced
152
133
  // with a new one but the old one is still in the state
@@ -157,15 +138,8 @@ export class Subscriber extends BasePeerConnection {
157
138
  [streamKindProp]: primaryStream,
158
139
  });
159
140
 
141
+ // now, dispose the previous stream if it exists
160
142
  if (previousStream) {
161
- if (isSelfSub && !this.trackedStreams.has(previousStream)) {
162
- // this is the local capture stream, we don't want to dispose it
163
- this.logger.debug(
164
- `[onTrack]: Skipping cleanup of previous ${e.track.kind} stream for userId: ${participantToUpdate.userId} because it is not tracked`,
165
- );
166
- return;
167
- }
168
-
169
143
  this.logger.info(
170
144
  `[onTrack]: Cleaning up previous remote ${track.kind} tracks for userId: ${participantToUpdate.userId}`,
171
145
  );
@@ -3,6 +3,7 @@ import './mocks/webrtc.mocks';
3
3
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
4
  import { Call } from '../../Call';
5
5
  import { StreamClient } from '../../coordinator/connection/client';
6
+ import { ClientEventReporter } from '../../reporting';
6
7
  import { StreamVideoWriteableStateStore } from '../../store';
7
8
  import { CallingState } from '../../store';
8
9
  import { NegotiationError } from '../NegotiationError';
@@ -30,6 +31,7 @@ const makeCall = () => {
30
31
  type: 'default',
31
32
  id: 'test-call',
32
33
  streamClient,
34
+ clientEventReporter: new ClientEventReporter({ streamClient }),
33
35
  clientStore,
34
36
  ringing: false,
35
37
  watching: false,
@@ -585,6 +587,7 @@ describe('Call reconnect wiring (PC event → leave)', () => {
585
587
  type: 'default',
586
588
  id: 'test-call',
587
589
  streamClient,
590
+ clientEventReporter: new ClientEventReporter({ streamClient }),
588
591
  clientStore,
589
592
  ringing: false,
590
593
  watching: false,
package/src/rtc/types.ts CHANGED
@@ -2,6 +2,7 @@ import {
2
2
  AudioBitrateProfile,
3
3
  PeerType,
4
4
  PublishOption,
5
+ TrackType,
5
6
  WebsocketReconnectStrategy,
6
7
  } from '../gen/video/sfu/models/models';
7
8
  import { StreamSfuClient } from '../StreamSfuClient';
@@ -53,6 +54,37 @@ export type OnReconnectionNeeded = (
53
54
  */
54
55
  export type OnIceConnected = (peerType: PeerType) => void;
55
56
 
57
+ /**
58
+ * Snapshot of the peer connection's ICE and DTLS state surfaced to telemetry
59
+ * consumers (e.g. `ClientEventReporter`). Fired on every transition of
60
+ * either `iceConnectionState` or `peerConnectionState`.
61
+ */
62
+ export type PeerConnectionStateChangeEvent =
63
+ | {
64
+ peerType: PeerType;
65
+ stateType: 'ice';
66
+ state: RTCIceConnectionState;
67
+ }
68
+ | {
69
+ peerType: PeerType;
70
+ stateType: 'peerConnection';
71
+ state: RTCPeerConnectionState;
72
+ };
73
+
74
+ export type OnPeerConnectionStateChange = (
75
+ event: PeerConnectionStateChangeEvent,
76
+ ) => void;
77
+
78
+ /**
79
+ * Fired when a remote track starts receiving media (`unmute`). Used by
80
+ * telemetry to report the `FirstVideoFrame` / `FirstAudioFrame` stage; the
81
+ * consumer decides which track types are relevant.
82
+ */
83
+ export type OnRemoteTrackUnmute = (
84
+ trackType: TrackType,
85
+ trackId: string,
86
+ ) => void;
87
+
56
88
  export type BasePeerConnectionOpts = {
57
89
  sfuClient: StreamSfuClient;
58
90
  state: CallState;
@@ -60,6 +92,8 @@ export type BasePeerConnectionOpts = {
60
92
  dispatcher: Dispatcher;
61
93
  onReconnectionNeeded?: OnReconnectionNeeded;
62
94
  onIceConnected?: OnIceConnected;
95
+ onPeerConnectionStateChange?: OnPeerConnectionStateChange;
96
+ onRemoteTrackUnmute?: OnRemoteTrackUnmute;
63
97
  tag: string;
64
98
  enableTracing: boolean;
65
99
  iceRestartDelay?: number;
package/src/types.ts CHANGED
@@ -14,6 +14,7 @@ import type {
14
14
  StartRecordingResponse,
15
15
  } from './gen/coordinator';
16
16
  import type { StreamClient } from './coordinator/connection/client';
17
+ import type { ClientEventReporter } from './reporting';
17
18
  import type {
18
19
  RejectReason,
19
20
  StreamClientOptions,
@@ -311,6 +312,11 @@ export type CallConstructor = {
311
312
  */
312
313
  streamClient: StreamClient;
313
314
 
315
+ /**
316
+ * The shared client event reporter, owned by `StreamVideoClient`.
317
+ */
318
+ clientEventReporter: ClientEventReporter;
319
+
314
320
  /**
315
321
  * The Call type.
316
322
  */
@@ -474,6 +480,18 @@ export type StreamRNVideoSDKGlobals = {
474
480
  * Stops the in call manager.
475
481
  */
476
482
  stop({ isRingingTypeCall }: StreamRNVideoSDKCallManagerRingingParams): void;
483
+
484
+ /**
485
+ * iOS-only. Keeps the audio engine's microphone-input (voice-processing)
486
+ * chain prepared while the mic is muted, so the `AVAudioEngine` stays full-duplex
487
+ * meaning: mic going out and speaker coming in, simultaneously
488
+ *
489
+ * Without this, joining muted builds an output-only engine under the
490
+ * `PlayAndRecord`/`VoiceChat` (VPIO) session, which makes the remote audio
491
+ * not audible when joining muted. As echo cancellation expects the mic to be
492
+ * prepared.
493
+ */
494
+ setMutedRecordingPrepared?(enabled: boolean): void;
477
495
  };
478
496
  permissions: {
479
497
  /**