@stream-io/video-client 0.0.1-alpha.7

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 (157) hide show
  1. package/LICENSE +219 -0
  2. package/README.md +14 -0
  3. package/dist/index.d.ts +23 -0
  4. package/dist/index.js +14663 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/src/Batcher.d.ts +12 -0
  7. package/dist/src/CallDropScheduler.d.ts +44 -0
  8. package/dist/src/StreamSfuClient.d.ts +25 -0
  9. package/dist/src/StreamVideoClient.d.ts +145 -0
  10. package/dist/src/__tests__/StreamVideoClient.test.d.ts +1 -0
  11. package/dist/src/config/defaultConfigs.d.ts +2 -0
  12. package/dist/src/config/types.d.ts +29 -0
  13. package/dist/src/coordinator/StreamCoordinatorClient.d.ts +19 -0
  14. package/dist/src/coordinator/connection/base64.d.ts +2 -0
  15. package/dist/src/coordinator/connection/client.d.ts +174 -0
  16. package/dist/src/coordinator/connection/connection.d.ts +139 -0
  17. package/dist/src/coordinator/connection/connection_fallback.d.ts +38 -0
  18. package/dist/src/coordinator/connection/errors.d.ts +16 -0
  19. package/dist/src/coordinator/connection/events.d.ts +7 -0
  20. package/dist/src/coordinator/connection/insights.d.ts +58 -0
  21. package/dist/src/coordinator/connection/signing.d.ts +30 -0
  22. package/dist/src/coordinator/connection/token_manager.d.ts +39 -0
  23. package/dist/src/coordinator/connection/types.d.ts +96 -0
  24. package/dist/src/coordinator/connection/utils.d.ts +25 -0
  25. package/dist/src/devices.d.ts +79 -0
  26. package/dist/src/events/call.d.ts +26 -0
  27. package/dist/src/events/internal.d.ts +8 -0
  28. package/dist/src/events/participant.d.ts +21 -0
  29. package/dist/src/events/speaker.d.ts +10 -0
  30. package/dist/src/gen/coordinator/index.d.ts +1664 -0
  31. package/dist/src/gen/google/protobuf/descriptor.d.ts +1650 -0
  32. package/dist/src/gen/google/protobuf/duration.d.ts +113 -0
  33. package/dist/src/gen/google/protobuf/struct.d.ts +184 -0
  34. package/dist/src/gen/google/protobuf/timestamp.d.ts +158 -0
  35. package/dist/src/gen/video/coordinator/broadcast_v1/broadcast.d.ts +66 -0
  36. package/dist/src/gen/video/coordinator/call_v1/call.d.ts +254 -0
  37. package/dist/src/gen/video/coordinator/client_v1_rpc/client_rpc.client.d.ts +351 -0
  38. package/dist/src/gen/video/coordinator/client_v1_rpc/client_rpc.d.ts +1488 -0
  39. package/dist/src/gen/video/coordinator/client_v1_rpc/envelopes.d.ts +143 -0
  40. package/dist/src/gen/video/coordinator/client_v1_rpc/websocket.d.ts +292 -0
  41. package/dist/src/gen/video/coordinator/edge_v1/edge.d.ts +183 -0
  42. package/dist/src/gen/video/coordinator/event_v1/event.d.ts +411 -0
  43. package/dist/src/gen/video/coordinator/geofence_v1/geofence.d.ts +63 -0
  44. package/dist/src/gen/video/coordinator/member_v1/member.d.ts +59 -0
  45. package/dist/src/gen/video/coordinator/participant_v1/participant.d.ts +103 -0
  46. package/dist/src/gen/video/coordinator/push_v1/push.d.ts +240 -0
  47. package/dist/src/gen/video/coordinator/stat_v1/stat.d.ts +308 -0
  48. package/dist/src/gen/video/coordinator/user_v1/user.d.ts +112 -0
  49. package/dist/src/gen/video/coordinator/utils_v1/utils.d.ts +47 -0
  50. package/dist/src/gen/video/sfu/event/events.d.ts +736 -0
  51. package/dist/src/gen/video/sfu/models/models.d.ts +460 -0
  52. package/dist/src/gen/video/sfu/signal_rpc/signal.client.d.ts +89 -0
  53. package/dist/src/gen/video/sfu/signal_rpc/signal.d.ts +320 -0
  54. package/dist/src/helpers/browsers.d.ts +8 -0
  55. package/dist/src/helpers/sound-detector.d.ts +34 -0
  56. package/dist/src/rpc/createClient.d.ts +10 -0
  57. package/dist/src/rpc/index.d.ts +2 -0
  58. package/dist/src/rpc/latency.d.ts +9 -0
  59. package/dist/src/rtc/Call.d.ts +180 -0
  60. package/dist/src/rtc/CallMetadata.d.ts +9 -0
  61. package/dist/src/rtc/Dispatcher.d.ts +9 -0
  62. package/dist/src/rtc/IceTrickleBuffer.d.ts +11 -0
  63. package/dist/src/rtc/callEventHandlers.d.ts +5 -0
  64. package/dist/src/rtc/codecs.d.ts +2 -0
  65. package/dist/src/rtc/helpers/iceCandidate.d.ts +2 -0
  66. package/dist/src/rtc/helpers/tracks.d.ts +3 -0
  67. package/dist/src/rtc/publisher.d.ts +53 -0
  68. package/dist/src/rtc/signal.d.ts +5 -0
  69. package/dist/src/rtc/subscriber.d.ts +7 -0
  70. package/dist/src/rtc/types.d.ts +84 -0
  71. package/dist/src/rtc/videoLayers.d.ts +17 -0
  72. package/dist/src/stats/coordinator-stats-reporter.d.ts +10 -0
  73. package/dist/src/stats/state-store-stats-reporter.d.ts +57 -0
  74. package/dist/src/stats/types.d.ts +42 -0
  75. package/dist/src/store/index.d.ts +2 -0
  76. package/dist/src/store/rxUtils.d.ts +18 -0
  77. package/dist/src/store/stateStore.d.ts +182 -0
  78. package/generate-openapi.sh +32 -0
  79. package/index.ts +30 -0
  80. package/openapitools.json +7 -0
  81. package/package.json +54 -0
  82. package/rollup.config.mjs +48 -0
  83. package/src/Batcher.ts +43 -0
  84. package/src/CallDropScheduler.ts +192 -0
  85. package/src/StreamSfuClient.ts +185 -0
  86. package/src/StreamVideoClient.ts +487 -0
  87. package/src/__tests__/StreamVideoClient.test.ts +83 -0
  88. package/src/config/defaultConfigs.ts +15 -0
  89. package/src/config/types.ts +30 -0
  90. package/src/coordinator/StreamCoordinatorClient.ts +111 -0
  91. package/src/coordinator/connection/base64.ts +80 -0
  92. package/src/coordinator/connection/client.ts +815 -0
  93. package/src/coordinator/connection/connection.ts +750 -0
  94. package/src/coordinator/connection/connection_fallback.ts +239 -0
  95. package/src/coordinator/connection/errors.ts +70 -0
  96. package/src/coordinator/connection/events.ts +10 -0
  97. package/src/coordinator/connection/insights.ts +88 -0
  98. package/src/coordinator/connection/signing.ts +104 -0
  99. package/src/coordinator/connection/token_manager.ts +160 -0
  100. package/src/coordinator/connection/types.ts +120 -0
  101. package/src/coordinator/connection/utils.ts +148 -0
  102. package/src/devices.ts +266 -0
  103. package/src/events/call.ts +166 -0
  104. package/src/events/internal.ts +47 -0
  105. package/src/events/participant.ts +97 -0
  106. package/src/events/speaker.ts +62 -0
  107. package/src/gen/coordinator/index.ts +1653 -0
  108. package/src/gen/google/protobuf/descriptor.ts +3466 -0
  109. package/src/gen/google/protobuf/duration.ts +232 -0
  110. package/src/gen/google/protobuf/struct.ts +481 -0
  111. package/src/gen/google/protobuf/timestamp.ts +291 -0
  112. package/src/gen/video/coordinator/broadcast_v1/broadcast.ts +154 -0
  113. package/src/gen/video/coordinator/call_v1/call.ts +651 -0
  114. package/src/gen/video/coordinator/client_v1_rpc/client_rpc.client.ts +463 -0
  115. package/src/gen/video/coordinator/client_v1_rpc/client_rpc.ts +3819 -0
  116. package/src/gen/video/coordinator/client_v1_rpc/envelopes.ts +424 -0
  117. package/src/gen/video/coordinator/client_v1_rpc/websocket.ts +719 -0
  118. package/src/gen/video/coordinator/edge_v1/edge.ts +532 -0
  119. package/src/gen/video/coordinator/event_v1/event.ts +1171 -0
  120. package/src/gen/video/coordinator/geofence_v1/geofence.ts +128 -0
  121. package/src/gen/video/coordinator/member_v1/member.ts +138 -0
  122. package/src/gen/video/coordinator/participant_v1/participant.ts +261 -0
  123. package/src/gen/video/coordinator/push_v1/push.ts +651 -0
  124. package/src/gen/video/coordinator/stat_v1/stat.ts +656 -0
  125. package/src/gen/video/coordinator/user_v1/user.ts +277 -0
  126. package/src/gen/video/coordinator/utils_v1/utils.ts +98 -0
  127. package/src/gen/video/sfu/event/events.ts +1962 -0
  128. package/src/gen/video/sfu/models/models.ts +1062 -0
  129. package/src/gen/video/sfu/signal_rpc/signal.client.ts +108 -0
  130. package/src/gen/video/sfu/signal_rpc/signal.ts +906 -0
  131. package/src/helpers/browsers.ts +13 -0
  132. package/src/helpers/sound-detector.ts +85 -0
  133. package/src/rpc/createClient.ts +50 -0
  134. package/src/rpc/index.ts +2 -0
  135. package/src/rpc/latency.ts +43 -0
  136. package/src/rtc/Call.ts +585 -0
  137. package/src/rtc/CallMetadata.ts +24 -0
  138. package/src/rtc/Dispatcher.ts +46 -0
  139. package/src/rtc/IceTrickleBuffer.ts +21 -0
  140. package/src/rtc/callEventHandlers.ts +37 -0
  141. package/src/rtc/codecs.ts +61 -0
  142. package/src/rtc/helpers/iceCandidate.ts +16 -0
  143. package/src/rtc/helpers/tracks.ts +18 -0
  144. package/src/rtc/publisher.ts +305 -0
  145. package/src/rtc/signal.ts +34 -0
  146. package/src/rtc/subscriber.ts +85 -0
  147. package/src/rtc/types.ts +105 -0
  148. package/src/rtc/videoLayers.ts +103 -0
  149. package/src/stats/coordinator-stats-reporter.ts +167 -0
  150. package/src/stats/state-store-stats-reporter.ts +364 -0
  151. package/src/stats/types.ts +46 -0
  152. package/src/store/index.ts +2 -0
  153. package/src/store/rxUtils.ts +42 -0
  154. package/src/store/stateStore.ts +341 -0
  155. package/tsconfig.json +25 -0
  156. package/typedoc.json +11 -0
  157. package/vite.config.ts +11 -0
@@ -0,0 +1,37 @@
1
+ import { Call } from './Call';
2
+ import { Dispatcher } from './Dispatcher';
3
+ import { StreamVideoWriteableStateStore } from '../store';
4
+ import {
5
+ watchParticipantJoined,
6
+ watchParticipantLeft,
7
+ watchTrackPublished,
8
+ watchTrackUnpublished,
9
+ } from '../events/participant';
10
+ import {
11
+ watchChangePublishQuality,
12
+ watchConnectionQualityChanged,
13
+ } from '../events/internal';
14
+ import {
15
+ watchAudioLevelChanged,
16
+ watchDominantSpeakerChanged,
17
+ } from '../events/speaker';
18
+ import { Batcher } from '../Batcher';
19
+
20
+ export const registerEventHandlers = (
21
+ call: Call,
22
+ store: StreamVideoWriteableStateStore,
23
+ dispatcher: Dispatcher,
24
+ userBatcher: Batcher<string>,
25
+ ) => {
26
+ watchChangePublishQuality(dispatcher, call);
27
+ watchConnectionQualityChanged(dispatcher, store);
28
+
29
+ watchParticipantJoined(dispatcher, store, userBatcher);
30
+ watchParticipantLeft(dispatcher, store);
31
+
32
+ watchTrackPublished(dispatcher, store);
33
+ watchTrackUnpublished(dispatcher, store);
34
+
35
+ watchAudioLevelChanged(dispatcher, store);
36
+ watchDominantSpeakerChanged(dispatcher, store);
37
+ };
@@ -0,0 +1,61 @@
1
+ export const getPreferredCodecs = (
2
+ kind: 'audio' | 'video',
3
+ videoCodec: string,
4
+ ) => {
5
+ if (!('getCapabilities' in RTCRtpSender)) {
6
+ console.warn('RTCRtpSender.getCapabilities is not supported');
7
+ return;
8
+ }
9
+ const cap = RTCRtpSender.getCapabilities(kind);
10
+ console.log('s4e');
11
+ if (!cap) return;
12
+ const matched: RTCRtpCodecCapability[] = [];
13
+ const partialMatched: RTCRtpCodecCapability[] = [];
14
+ const unmatched: RTCRtpCodecCapability[] = [];
15
+ cap.codecs.forEach((c) => {
16
+ const codec = c.mimeType.toLowerCase();
17
+ if (codec === 'audio/opus') {
18
+ matched.push(c);
19
+ return;
20
+ }
21
+ console.log(c);
22
+ const matchesVideoCodec = codec === `video/${videoCodec}`;
23
+ if (!matchesVideoCodec) {
24
+ unmatched.push(c);
25
+ return;
26
+ }
27
+ // for h264 codecs that have sdpFmtpLine available, use only if the
28
+ // profile-level-id is 42e01f for cross-browser compatibility
29
+ if (videoCodec === 'h264') {
30
+ if (c.sdpFmtpLine && c.sdpFmtpLine.includes('profile-level-id=42e01f')) {
31
+ matched.push(c);
32
+ } else {
33
+ partialMatched.push(c);
34
+ }
35
+ return;
36
+ }
37
+ console.log('matched', matched);
38
+ matched.push(c);
39
+ });
40
+
41
+ return [
42
+ ...matched,
43
+ ...partialMatched,
44
+ ...unmatched,
45
+ ] as RTCRtpCodecCapability[];
46
+ };
47
+
48
+ export const getGenericSdp = async (direction: RTCRtpTransceiverDirection) => {
49
+ const tempPc = new RTCPeerConnection();
50
+ tempPc.addTransceiver('audio', { direction });
51
+ tempPc.addTransceiver('video', { direction });
52
+
53
+ const offer = await tempPc.createOffer();
54
+ const sdp = offer.sdp;
55
+
56
+ tempPc.getTransceivers().forEach((t) => {
57
+ t.stop();
58
+ });
59
+ tempPc.close();
60
+ return sdp;
61
+ };
@@ -0,0 +1,16 @@
1
+ import { ICETrickle } from '../../gen/video/sfu/models/models';
2
+
3
+ export function getIceCandidate(
4
+ candidate: RTCIceCandidate,
5
+ ): ICETrickle['iceCandidate'] {
6
+ if (!candidate.usernameFragment) {
7
+ // react-native-webrtc doesn't include usernameFragment in the candidate
8
+ const splittedCandidate = candidate.candidate.split(' ');
9
+ const ufragIndex =
10
+ splittedCandidate.findIndex((s: string) => s === 'ufrag') + 1;
11
+ const usernameFragment = splittedCandidate[ufragIndex];
12
+ return JSON.stringify({ ...candidate, usernameFragment });
13
+ } else {
14
+ return JSON.stringify(candidate.toJSON());
15
+ }
16
+ }
@@ -0,0 +1,18 @@
1
+ import { TrackType } from '../../gen/video/sfu/models/models';
2
+ import type { StreamVideoParticipant } from '../types';
3
+
4
+ export const trackTypeToParticipantStreamKey = (
5
+ trackType: TrackType,
6
+ ): keyof StreamVideoParticipant | undefined => {
7
+ switch (trackType) {
8
+ case TrackType.SCREEN_SHARE:
9
+ return 'screenShareStream';
10
+ case TrackType.VIDEO:
11
+ return 'videoStream';
12
+ case TrackType.AUDIO:
13
+ return 'audioStream';
14
+ default:
15
+ console.error(`Unknown track type: ${trackType}`);
16
+ return undefined;
17
+ }
18
+ };
@@ -0,0 +1,305 @@
1
+ import { StreamSfuClient } from '../StreamSfuClient';
2
+ import {
3
+ PeerType,
4
+ TrackInfo,
5
+ TrackType,
6
+ VideoLayer,
7
+ VideoQuality,
8
+ } from '../gen/video/sfu/models/models';
9
+ import { getIceCandidate } from './helpers/iceCandidate';
10
+ import {
11
+ findOptimalVideoLayers,
12
+ findOptimalScreenSharingLayers,
13
+ } from './videoLayers';
14
+ import { getPreferredCodecs } from './codecs';
15
+ import { PublishOptions } from './types';
16
+
17
+ export type PublisherOpts = {
18
+ rpcClient: StreamSfuClient;
19
+ connectionConfig?: RTCConfiguration;
20
+ };
21
+
22
+ /**
23
+ * @internal
24
+ * The `Publisher` is responsible for publishing/unpublishing media streams to/from the SFU
25
+ */
26
+ export class Publisher {
27
+ private readonly publisher: RTCPeerConnection;
28
+ private readonly rpcClient: StreamSfuClient;
29
+ private readonly transceiverRegistry: {
30
+ [key in TrackType]: RTCRtpTransceiver | undefined;
31
+ } = {
32
+ [TrackType.AUDIO]: undefined,
33
+ [TrackType.VIDEO]: undefined,
34
+ [TrackType.SCREEN_SHARE]: undefined,
35
+ [TrackType.SCREEN_SHARE_AUDIO]: undefined,
36
+ [TrackType.UNSPECIFIED]: undefined,
37
+ };
38
+ private readonly trackKindRegistry: {
39
+ [key in TrackType]: 'video' | 'audio' | undefined;
40
+ } = {
41
+ [TrackType.AUDIO]: 'audio',
42
+ [TrackType.VIDEO]: 'video',
43
+ [TrackType.SCREEN_SHARE]: 'video',
44
+ [TrackType.SCREEN_SHARE_AUDIO]: undefined,
45
+ [TrackType.UNSPECIFIED]: undefined,
46
+ };
47
+
48
+ constructor({ connectionConfig, rpcClient }: PublisherOpts) {
49
+ const pc = new RTCPeerConnection(connectionConfig);
50
+ pc.addEventListener('icecandidate', this.onIceCandidate);
51
+ pc.addEventListener('negotiationneeded', this.onNegotiationNeeded);
52
+
53
+ pc.addEventListener('icecandidateerror', this.onIceCandidateError);
54
+ pc.addEventListener(
55
+ 'iceconnectionstatechange',
56
+ this.onIceConnectionStateChange,
57
+ );
58
+ pc.addEventListener(
59
+ 'icegatheringstatechange',
60
+ this.onIceGatheringStateChange,
61
+ );
62
+
63
+ this.publisher = pc;
64
+ this.rpcClient = rpcClient;
65
+ }
66
+
67
+ /**
68
+ * Starts publishing the given track of the given media stream.
69
+ *
70
+ * Consecutive calls to this method will replace the stream.
71
+ * The previous stream will be stopped.
72
+ * @param mediaStream
73
+ * @param track
74
+ * @param trackType
75
+ * @param opts
76
+ */
77
+ publishStream = async (
78
+ mediaStream: MediaStream,
79
+ track: MediaStreamTrack,
80
+ trackType: TrackType,
81
+ opts: PublishOptions = {},
82
+ ) => {
83
+ let transceiver = this.publisher
84
+ .getTransceivers()
85
+ .find(
86
+ (t) =>
87
+ t === this.transceiverRegistry[trackType] &&
88
+ t.sender.track &&
89
+ t.sender.track?.kind === this.trackKindRegistry[trackType],
90
+ );
91
+
92
+ if (!transceiver) {
93
+ let videoEncodings: RTCRtpEncodingParameters[] | undefined;
94
+ if (trackType === TrackType.VIDEO) {
95
+ videoEncodings = findOptimalVideoLayers(track);
96
+ }
97
+ transceiver = this.publisher.addTransceiver(track, {
98
+ direction: 'sendonly',
99
+ streams:
100
+ trackType === TrackType.VIDEO || trackType === TrackType.SCREEN_SHARE
101
+ ? [mediaStream]
102
+ : undefined,
103
+ sendEncodings:
104
+ trackType === TrackType.VIDEO ? videoEncodings : undefined,
105
+ });
106
+
107
+ this.transceiverRegistry[trackType] = transceiver;
108
+
109
+ if (trackType === TrackType.VIDEO) {
110
+ const codecPreferences = getPreferredCodecs(
111
+ 'video',
112
+ opts.preferredCodec || 'vp8',
113
+ );
114
+
115
+ if ('setCodecPreferences' in transceiver && codecPreferences) {
116
+ console.log(`set codec preferences`, codecPreferences);
117
+ // @ts-ignore
118
+ transceiver.setCodecPreferences(codecPreferences);
119
+ }
120
+ }
121
+ } else {
122
+ transceiver.sender.track?.stop();
123
+ await transceiver.sender.replaceTrack(track);
124
+ }
125
+ };
126
+
127
+ /**
128
+ * Stops publishing the given track type to the SFU, if it is currently being published.
129
+ * Underlying track will be stopped and removed from the publisher.
130
+ * @param trackType
131
+ * @returns `true` if track with the given track type was found, otherwise `false`
132
+ */
133
+ unpublishStream = (trackType: TrackType) => {
134
+ const transceiver = this.publisher
135
+ .getTransceivers()
136
+ .find((t) => t === this.transceiverRegistry[trackType] && t.sender.track);
137
+ if (transceiver && transceiver.sender.track) {
138
+ transceiver.sender.track.stop();
139
+ return true;
140
+ } else {
141
+ return false;
142
+ }
143
+ };
144
+
145
+ /**
146
+ * Stops publishing all tracks and stop all tracks.
147
+ */
148
+ stopPublishing = () => {
149
+ this.publisher.getSenders().forEach((s) => {
150
+ if (s.track) {
151
+ s.track.stop();
152
+ }
153
+ if (this.publisher.signalingState !== 'closed') {
154
+ this.publisher.removeTrack(s);
155
+ }
156
+ });
157
+ this.publisher.close();
158
+ };
159
+
160
+ updateVideoPublishQuality = async (enabledRids: string[]) => {
161
+ console.log(
162
+ 'Updating publish quality, qualities requested by SFU:',
163
+ enabledRids,
164
+ );
165
+
166
+ const videoSender = this.transceiverRegistry[TrackType.VIDEO]?.sender;
167
+
168
+ if (!videoSender) return;
169
+
170
+ const params = videoSender.getParameters();
171
+ let changed = false;
172
+ params.encodings.forEach((enc) => {
173
+ console.log(enc.rid, enc.active);
174
+ // flip 'active' flag only when necessary
175
+ const shouldEnable = enabledRids.includes(enc.rid!);
176
+ if (shouldEnable !== enc.active) {
177
+ enc.active = shouldEnable;
178
+ changed = true;
179
+ }
180
+ });
181
+ if (changed) {
182
+ if (params.encodings.length === 0) {
183
+ console.warn('No suitable video encoding quality found');
184
+ }
185
+ await videoSender.setParameters(params);
186
+ }
187
+ };
188
+
189
+ /**
190
+ * Returns the result of the `RTCPeerConnection.getStats()` method
191
+ * @param selector
192
+ * @returns
193
+ */
194
+ getStats(selector?: MediaStreamTrack | null | undefined) {
195
+ return this.publisher.getStats(selector);
196
+ }
197
+
198
+ private onIceCandidate = async (e: RTCPeerConnectionIceEvent) => {
199
+ const { candidate } = e;
200
+ if (!candidate) {
201
+ console.log('null ice candidate');
202
+ return;
203
+ }
204
+ await this.rpcClient.iceTrickle({
205
+ iceCandidate: getIceCandidate(candidate),
206
+ peerType: PeerType.PUBLISHER_UNSPECIFIED,
207
+ });
208
+ };
209
+
210
+ private onNegotiationNeeded = async () => {
211
+ console.log('AAA onNegotiationNeeded');
212
+ const offer = await this.publisher.createOffer();
213
+ await this.publisher.setLocalDescription(offer);
214
+
215
+ const trackInfos = this.publisher
216
+ .getTransceivers()
217
+ .filter((t) => t.direction === 'sendonly' && !!t.sender.track)
218
+ .map<TrackInfo>((transceiver) => {
219
+ const trackType = Number(
220
+ Object.keys(this.transceiverRegistry).find(
221
+ (key) =>
222
+ this.transceiverRegistry[key as any as TrackType] === transceiver,
223
+ ),
224
+ );
225
+ const track = transceiver.sender.track!;
226
+ const optimalLayers =
227
+ trackType === TrackType.VIDEO
228
+ ? findOptimalVideoLayers(track)
229
+ : trackType === TrackType.SCREEN_SHARE
230
+ ? findOptimalScreenSharingLayers(track)
231
+ : [];
232
+
233
+ const layers = optimalLayers.map<VideoLayer>((optimalLayer) => ({
234
+ rid: optimalLayer.rid || '',
235
+ bitrate: optimalLayer.maxBitrate || 0,
236
+ fps: optimalLayer.maxFramerate || 0,
237
+ quality: this.ridToVideoQuality(optimalLayer.rid || ''),
238
+ videoDimension: {
239
+ width: optimalLayer.width,
240
+ height: optimalLayer.height,
241
+ },
242
+ }));
243
+
244
+ return {
245
+ trackId: track.id,
246
+ layers: layers,
247
+ trackType,
248
+ mid: transceiver.mid || '',
249
+ };
250
+ });
251
+
252
+ // TODO debounce for 250ms
253
+ const response = await this.rpcClient.setPublisher({
254
+ sdp: offer.sdp || '',
255
+ tracks: trackInfos,
256
+ });
257
+
258
+ await this.publisher.setRemoteDescription({
259
+ type: 'answer',
260
+ sdp: response.response.sdp,
261
+ });
262
+
263
+ this.rpcClient.iceTrickleBuffer.publisherCandidates.subscribe(
264
+ async (candidate) => {
265
+ try {
266
+ const iceCandidate = JSON.parse(candidate.iceCandidate);
267
+ await this.publisher.addIceCandidate(iceCandidate);
268
+ } catch (e) {
269
+ console.error(`[Publisher] ICE candidate error`, e, candidate);
270
+ }
271
+ },
272
+ );
273
+ };
274
+
275
+ private onIceCandidateError = (e: Event) => {
276
+ const errorMessage =
277
+ e instanceof RTCPeerConnectionIceErrorEvent &&
278
+ `${e.errorCode}: ${e.errorText}`;
279
+ console.error(`Publisher: ICE Candidate error`, errorMessage, e);
280
+ };
281
+
282
+ private onIceConnectionStateChange = (e: Event) => {
283
+ console.log(
284
+ `Publisher: ICE Connection state changed`,
285
+ this.publisher.iceConnectionState,
286
+ e,
287
+ );
288
+ };
289
+
290
+ private onIceGatheringStateChange = (e: Event) => {
291
+ console.log(
292
+ `Publisher: ICE Gathering State`,
293
+ this.publisher.iceGatheringState,
294
+ e,
295
+ );
296
+ };
297
+
298
+ private ridToVideoQuality = (rid: string): VideoQuality => {
299
+ return rid === 'q'
300
+ ? VideoQuality.LOW_UNSPECIFIED
301
+ : rid === 'h'
302
+ ? VideoQuality.MID
303
+ : VideoQuality.HIGH; // default to HIGH
304
+ };
305
+ }
@@ -0,0 +1,34 @@
1
+ import { SfuEvent } from '../gen/video/sfu/event/events';
2
+
3
+ export const createWebSocketSignalChannel = (opts: {
4
+ endpoint: string;
5
+ onMessage?: (message: SfuEvent) => void;
6
+ }) => {
7
+ return new Promise<WebSocket>((resolve) => {
8
+ const { endpoint, onMessage } = opts;
9
+ const ws = new WebSocket(endpoint);
10
+ ws.binaryType = 'arraybuffer'; // do we need this?
11
+ ws.addEventListener('open', () => {
12
+ return resolve(ws);
13
+ });
14
+
15
+ ws.addEventListener('error', (e) => {
16
+ console.error('Error', e);
17
+ });
18
+
19
+ ws.addEventListener('close', (e) => {
20
+ console.warn('Signalling channel is closed', e);
21
+ });
22
+
23
+ if (onMessage) {
24
+ ws.addEventListener('message', (e) => {
25
+ const message =
26
+ e.data instanceof ArrayBuffer
27
+ ? SfuEvent.fromBinary(new Uint8Array(e.data))
28
+ : SfuEvent.fromJsonString(e.data);
29
+
30
+ onMessage(message);
31
+ });
32
+ }
33
+ });
34
+ };
@@ -0,0 +1,85 @@
1
+ import { StreamSfuClient } from '../StreamSfuClient';
2
+ import { getIceCandidate } from './helpers/iceCandidate';
3
+ import { PeerType } from '../gen/video/sfu/models/models';
4
+
5
+ export type SubscriberOpts = {
6
+ rpcClient: StreamSfuClient;
7
+ connectionConfig?: RTCConfiguration;
8
+ onTrack?: (e: RTCTrackEvent) => void;
9
+ };
10
+
11
+ export const createSubscriber = ({
12
+ rpcClient,
13
+ connectionConfig,
14
+ onTrack,
15
+ }: SubscriberOpts) => {
16
+ const subscriber = new RTCPeerConnection(connectionConfig);
17
+ subscriber.addEventListener('icecandidate', async (e) => {
18
+ const { candidate } = e;
19
+ if (!candidate) {
20
+ console.log('null ice candidate');
21
+ return;
22
+ }
23
+
24
+ await rpcClient.iceTrickle({
25
+ iceCandidate: getIceCandidate(candidate),
26
+ peerType: PeerType.SUBSCRIBER,
27
+ });
28
+ });
29
+ subscriber.addEventListener('icecandidateerror', (e) => {
30
+ const errorMessage =
31
+ e instanceof RTCPeerConnectionIceErrorEvent &&
32
+ `${e.errorCode}: ${e.errorText}`;
33
+ console.error(`Subscriber: ICE Candidate error`, errorMessage, e);
34
+ });
35
+ subscriber.addEventListener('iceconnectionstatechange', (e) => {
36
+ console.log(
37
+ `Subscriber: ICE Connection state changed`,
38
+ subscriber.iceConnectionState,
39
+ e,
40
+ );
41
+ });
42
+ subscriber.addEventListener('icegatheringstatechange', (e) => {
43
+ console.log(
44
+ `Subscriber: ICE Gathering State`,
45
+ subscriber.iceGatheringState,
46
+ e,
47
+ );
48
+ });
49
+
50
+ if (onTrack) {
51
+ subscriber.addEventListener('track', onTrack);
52
+ }
53
+
54
+ const { dispatcher, iceTrickleBuffer } = rpcClient;
55
+ dispatcher.on('subscriberOffer', async (message) => {
56
+ if (message.eventPayload.oneofKind !== 'subscriberOffer') return;
57
+ const { subscriberOffer } = message.eventPayload;
58
+ console.log(`Received subscriberOffer`, subscriberOffer);
59
+
60
+ await subscriber.setRemoteDescription({
61
+ type: 'offer',
62
+ sdp: subscriberOffer.sdp,
63
+ });
64
+
65
+ iceTrickleBuffer.subscriberCandidates.subscribe(async (candidate) => {
66
+ try {
67
+ const iceCandidate = JSON.parse(candidate.iceCandidate);
68
+ await subscriber.addIceCandidate(iceCandidate);
69
+ } catch (e) {
70
+ console.error(`[Subscriber] ICE candidate error`, e, candidate);
71
+ }
72
+ });
73
+
74
+ // apply ice candidates
75
+ const answer = await subscriber.createAnswer();
76
+ await subscriber.setLocalDescription(answer);
77
+
78
+ await rpcClient.sendAnswer({
79
+ peerType: PeerType.SUBSCRIBER,
80
+ sdp: answer.sdp || '',
81
+ });
82
+ });
83
+
84
+ return subscriber;
85
+ };
@@ -0,0 +1,105 @@
1
+ import type { UserResponse } from '../gen/coordinator';
2
+ import type {
3
+ Participant,
4
+ VideoDimension,
5
+ } from '../gen/video/sfu/models/models';
6
+
7
+ export interface StreamVideoParticipant extends Participant {
8
+ /**
9
+ * The participant's audio stream, if they are publishing audio and
10
+ * we have subscribed to it.
11
+ */
12
+ audioStream?: MediaStream;
13
+
14
+ /**
15
+ * The participant's video stream, if they are sharing their video,
16
+ * and we are subscribed to it.
17
+ */
18
+ videoStream?: MediaStream;
19
+
20
+ /**
21
+ * The participant's screen share stream, if they are sharing their screen,
22
+ * and we are subscribed to it.
23
+ */
24
+ screenShareStream?: MediaStream;
25
+
26
+ /**
27
+ * The preferred video dimensions for this participant.
28
+ * Set it to `undefined` to unsubscribe from this participant's video.
29
+ */
30
+ videoDimension?: VideoDimension;
31
+
32
+ /**
33
+ * The preferred screen share dimensions for this participant.
34
+ * Set it to `undefined` to unsubscribe from this participant's screen share.
35
+ */
36
+ screenShareDimension?: VideoDimension;
37
+
38
+ /**
39
+ * True if the participant is the local participant.
40
+ */
41
+ isLoggedInUser?: boolean;
42
+
43
+ /**
44
+ * True when the participant is pinned
45
+ */
46
+ isPinned?: boolean;
47
+
48
+ /**
49
+ * User metadata (profile picture, name...)
50
+ */
51
+ user?: UserResponse;
52
+ }
53
+
54
+ export interface StreamVideoLocalParticipant extends StreamVideoParticipant {
55
+ /**
56
+ * The device ID of the currently selected audio input device of the local participant (returned by the [MediaDevices API](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia))
57
+ */
58
+ audioDeviceId?: string;
59
+ /**
60
+ * The device ID of the currently selected video input device of the local participant (returned by the [MediaDevices API](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia))
61
+ */
62
+ videoDeviceId?: string;
63
+
64
+ /**
65
+ * The device ID of the currently selected audio output device of the local participant (returned by the [MediaDevices API](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia))
66
+ *
67
+ * If the value is not defined, the user hasn't selected any device (in these cases the default system audio output could be used)
68
+ */
69
+ audioOutputDeviceId?: string;
70
+ }
71
+
72
+ /**
73
+ * A partial representation of the StreamVideoParticipant.
74
+ */
75
+ export type StreamVideoParticipantPatch = Partial<
76
+ StreamVideoParticipant | StreamVideoLocalParticipant
77
+ >;
78
+
79
+ /**
80
+ * A collection of {@link StreamVideoParticipantPatch} organized by sessionId.
81
+ */
82
+ export type StreamVideoParticipantPatches = {
83
+ [sessionId: string]: StreamVideoParticipantPatch;
84
+ };
85
+
86
+ export type SubscriptionChange = {
87
+ /**
88
+ * The video dimension to request.
89
+ * Set it to `undefined` in case you want to unsubscribe.
90
+ */
91
+ dimension: VideoDimension | undefined;
92
+ };
93
+
94
+ export type SubscriptionChanges = {
95
+ [sessionId: string]: SubscriptionChange;
96
+ };
97
+
98
+ export type CallOptions = {
99
+ connectionConfig?: RTCConfiguration;
100
+ edgeName?: string;
101
+ };
102
+
103
+ export type PublishOptions = {
104
+ preferredCodec?: string | null;
105
+ };