@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.
- package/LICENSE +219 -0
- package/README.md +14 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +14663 -0
- package/dist/index.js.map +1 -0
- package/dist/src/Batcher.d.ts +12 -0
- package/dist/src/CallDropScheduler.d.ts +44 -0
- package/dist/src/StreamSfuClient.d.ts +25 -0
- package/dist/src/StreamVideoClient.d.ts +145 -0
- package/dist/src/__tests__/StreamVideoClient.test.d.ts +1 -0
- package/dist/src/config/defaultConfigs.d.ts +2 -0
- package/dist/src/config/types.d.ts +29 -0
- package/dist/src/coordinator/StreamCoordinatorClient.d.ts +19 -0
- package/dist/src/coordinator/connection/base64.d.ts +2 -0
- package/dist/src/coordinator/connection/client.d.ts +174 -0
- package/dist/src/coordinator/connection/connection.d.ts +139 -0
- package/dist/src/coordinator/connection/connection_fallback.d.ts +38 -0
- package/dist/src/coordinator/connection/errors.d.ts +16 -0
- package/dist/src/coordinator/connection/events.d.ts +7 -0
- package/dist/src/coordinator/connection/insights.d.ts +58 -0
- package/dist/src/coordinator/connection/signing.d.ts +30 -0
- package/dist/src/coordinator/connection/token_manager.d.ts +39 -0
- package/dist/src/coordinator/connection/types.d.ts +96 -0
- package/dist/src/coordinator/connection/utils.d.ts +25 -0
- package/dist/src/devices.d.ts +79 -0
- package/dist/src/events/call.d.ts +26 -0
- package/dist/src/events/internal.d.ts +8 -0
- package/dist/src/events/participant.d.ts +21 -0
- package/dist/src/events/speaker.d.ts +10 -0
- package/dist/src/gen/coordinator/index.d.ts +1664 -0
- package/dist/src/gen/google/protobuf/descriptor.d.ts +1650 -0
- package/dist/src/gen/google/protobuf/duration.d.ts +113 -0
- package/dist/src/gen/google/protobuf/struct.d.ts +184 -0
- package/dist/src/gen/google/protobuf/timestamp.d.ts +158 -0
- package/dist/src/gen/video/coordinator/broadcast_v1/broadcast.d.ts +66 -0
- package/dist/src/gen/video/coordinator/call_v1/call.d.ts +254 -0
- package/dist/src/gen/video/coordinator/client_v1_rpc/client_rpc.client.d.ts +351 -0
- package/dist/src/gen/video/coordinator/client_v1_rpc/client_rpc.d.ts +1488 -0
- package/dist/src/gen/video/coordinator/client_v1_rpc/envelopes.d.ts +143 -0
- package/dist/src/gen/video/coordinator/client_v1_rpc/websocket.d.ts +292 -0
- package/dist/src/gen/video/coordinator/edge_v1/edge.d.ts +183 -0
- package/dist/src/gen/video/coordinator/event_v1/event.d.ts +411 -0
- package/dist/src/gen/video/coordinator/geofence_v1/geofence.d.ts +63 -0
- package/dist/src/gen/video/coordinator/member_v1/member.d.ts +59 -0
- package/dist/src/gen/video/coordinator/participant_v1/participant.d.ts +103 -0
- package/dist/src/gen/video/coordinator/push_v1/push.d.ts +240 -0
- package/dist/src/gen/video/coordinator/stat_v1/stat.d.ts +308 -0
- package/dist/src/gen/video/coordinator/user_v1/user.d.ts +112 -0
- package/dist/src/gen/video/coordinator/utils_v1/utils.d.ts +47 -0
- package/dist/src/gen/video/sfu/event/events.d.ts +736 -0
- package/dist/src/gen/video/sfu/models/models.d.ts +460 -0
- package/dist/src/gen/video/sfu/signal_rpc/signal.client.d.ts +89 -0
- package/dist/src/gen/video/sfu/signal_rpc/signal.d.ts +320 -0
- package/dist/src/helpers/browsers.d.ts +8 -0
- package/dist/src/helpers/sound-detector.d.ts +34 -0
- package/dist/src/rpc/createClient.d.ts +10 -0
- package/dist/src/rpc/index.d.ts +2 -0
- package/dist/src/rpc/latency.d.ts +9 -0
- package/dist/src/rtc/Call.d.ts +180 -0
- package/dist/src/rtc/CallMetadata.d.ts +9 -0
- package/dist/src/rtc/Dispatcher.d.ts +9 -0
- package/dist/src/rtc/IceTrickleBuffer.d.ts +11 -0
- package/dist/src/rtc/callEventHandlers.d.ts +5 -0
- package/dist/src/rtc/codecs.d.ts +2 -0
- package/dist/src/rtc/helpers/iceCandidate.d.ts +2 -0
- package/dist/src/rtc/helpers/tracks.d.ts +3 -0
- package/dist/src/rtc/publisher.d.ts +53 -0
- package/dist/src/rtc/signal.d.ts +5 -0
- package/dist/src/rtc/subscriber.d.ts +7 -0
- package/dist/src/rtc/types.d.ts +84 -0
- package/dist/src/rtc/videoLayers.d.ts +17 -0
- package/dist/src/stats/coordinator-stats-reporter.d.ts +10 -0
- package/dist/src/stats/state-store-stats-reporter.d.ts +57 -0
- package/dist/src/stats/types.d.ts +42 -0
- package/dist/src/store/index.d.ts +2 -0
- package/dist/src/store/rxUtils.d.ts +18 -0
- package/dist/src/store/stateStore.d.ts +182 -0
- package/generate-openapi.sh +32 -0
- package/index.ts +30 -0
- package/openapitools.json +7 -0
- package/package.json +54 -0
- package/rollup.config.mjs +48 -0
- package/src/Batcher.ts +43 -0
- package/src/CallDropScheduler.ts +192 -0
- package/src/StreamSfuClient.ts +185 -0
- package/src/StreamVideoClient.ts +487 -0
- package/src/__tests__/StreamVideoClient.test.ts +83 -0
- package/src/config/defaultConfigs.ts +15 -0
- package/src/config/types.ts +30 -0
- package/src/coordinator/StreamCoordinatorClient.ts +111 -0
- package/src/coordinator/connection/base64.ts +80 -0
- package/src/coordinator/connection/client.ts +815 -0
- package/src/coordinator/connection/connection.ts +750 -0
- package/src/coordinator/connection/connection_fallback.ts +239 -0
- package/src/coordinator/connection/errors.ts +70 -0
- package/src/coordinator/connection/events.ts +10 -0
- package/src/coordinator/connection/insights.ts +88 -0
- package/src/coordinator/connection/signing.ts +104 -0
- package/src/coordinator/connection/token_manager.ts +160 -0
- package/src/coordinator/connection/types.ts +120 -0
- package/src/coordinator/connection/utils.ts +148 -0
- package/src/devices.ts +266 -0
- package/src/events/call.ts +166 -0
- package/src/events/internal.ts +47 -0
- package/src/events/participant.ts +97 -0
- package/src/events/speaker.ts +62 -0
- package/src/gen/coordinator/index.ts +1653 -0
- package/src/gen/google/protobuf/descriptor.ts +3466 -0
- package/src/gen/google/protobuf/duration.ts +232 -0
- package/src/gen/google/protobuf/struct.ts +481 -0
- package/src/gen/google/protobuf/timestamp.ts +291 -0
- package/src/gen/video/coordinator/broadcast_v1/broadcast.ts +154 -0
- package/src/gen/video/coordinator/call_v1/call.ts +651 -0
- package/src/gen/video/coordinator/client_v1_rpc/client_rpc.client.ts +463 -0
- package/src/gen/video/coordinator/client_v1_rpc/client_rpc.ts +3819 -0
- package/src/gen/video/coordinator/client_v1_rpc/envelopes.ts +424 -0
- package/src/gen/video/coordinator/client_v1_rpc/websocket.ts +719 -0
- package/src/gen/video/coordinator/edge_v1/edge.ts +532 -0
- package/src/gen/video/coordinator/event_v1/event.ts +1171 -0
- package/src/gen/video/coordinator/geofence_v1/geofence.ts +128 -0
- package/src/gen/video/coordinator/member_v1/member.ts +138 -0
- package/src/gen/video/coordinator/participant_v1/participant.ts +261 -0
- package/src/gen/video/coordinator/push_v1/push.ts +651 -0
- package/src/gen/video/coordinator/stat_v1/stat.ts +656 -0
- package/src/gen/video/coordinator/user_v1/user.ts +277 -0
- package/src/gen/video/coordinator/utils_v1/utils.ts +98 -0
- package/src/gen/video/sfu/event/events.ts +1962 -0
- package/src/gen/video/sfu/models/models.ts +1062 -0
- package/src/gen/video/sfu/signal_rpc/signal.client.ts +108 -0
- package/src/gen/video/sfu/signal_rpc/signal.ts +906 -0
- package/src/helpers/browsers.ts +13 -0
- package/src/helpers/sound-detector.ts +85 -0
- package/src/rpc/createClient.ts +50 -0
- package/src/rpc/index.ts +2 -0
- package/src/rpc/latency.ts +43 -0
- package/src/rtc/Call.ts +585 -0
- package/src/rtc/CallMetadata.ts +24 -0
- package/src/rtc/Dispatcher.ts +46 -0
- package/src/rtc/IceTrickleBuffer.ts +21 -0
- package/src/rtc/callEventHandlers.ts +37 -0
- package/src/rtc/codecs.ts +61 -0
- package/src/rtc/helpers/iceCandidate.ts +16 -0
- package/src/rtc/helpers/tracks.ts +18 -0
- package/src/rtc/publisher.ts +305 -0
- package/src/rtc/signal.ts +34 -0
- package/src/rtc/subscriber.ts +85 -0
- package/src/rtc/types.ts +105 -0
- package/src/rtc/videoLayers.ts +103 -0
- package/src/stats/coordinator-stats-reporter.ts +167 -0
- package/src/stats/state-store-stats-reporter.ts +364 -0
- package/src/stats/types.ts +46 -0
- package/src/store/index.ts +2 -0
- package/src/store/rxUtils.ts +42 -0
- package/src/store/stateStore.ts +341 -0
- package/tsconfig.json +25 -0
- package/typedoc.json +11 -0
- package/vite.config.ts +11 -0
package/src/rtc/Call.ts
ADDED
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
import { StreamSfuClient } from '../StreamSfuClient';
|
|
2
|
+
import { createSubscriber } from './subscriber';
|
|
3
|
+
import { Publisher } from './publisher';
|
|
4
|
+
import { getGenericSdp } from './codecs';
|
|
5
|
+
import { CallState, TrackType } from '../gen/video/sfu/models/models';
|
|
6
|
+
import { registerEventHandlers } from './callEventHandlers';
|
|
7
|
+
import { SfuEventListener } from './Dispatcher';
|
|
8
|
+
import { StreamVideoWriteableStateStore } from '../store';
|
|
9
|
+
import { trackTypeToParticipantStreamKey } from './helpers/tracks';
|
|
10
|
+
import type {
|
|
11
|
+
CallOptions,
|
|
12
|
+
PublishOptions,
|
|
13
|
+
StreamVideoParticipant,
|
|
14
|
+
StreamVideoParticipantPatches,
|
|
15
|
+
SubscriptionChanges,
|
|
16
|
+
} from './types';
|
|
17
|
+
import {
|
|
18
|
+
BehaviorSubject,
|
|
19
|
+
debounceTime,
|
|
20
|
+
filter,
|
|
21
|
+
Subject,
|
|
22
|
+
takeWhile,
|
|
23
|
+
} from 'rxjs';
|
|
24
|
+
import { TrackSubscriptionDetails } from '../gen/video/sfu/signal_rpc/signal';
|
|
25
|
+
import {
|
|
26
|
+
createStatsReporter,
|
|
27
|
+
StatsReporter,
|
|
28
|
+
} from '../stats/state-store-stats-reporter';
|
|
29
|
+
import { Batcher } from '../Batcher';
|
|
30
|
+
import { CallMetadata } from './CallMetadata';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A `Call` object represents the active call the user is part of. It's not enough to have a `Call` instance, you will also need to call the [`join`](#join) method.
|
|
34
|
+
*/
|
|
35
|
+
export class Call {
|
|
36
|
+
/**
|
|
37
|
+
* Contains metadata about the call, for example who created the call. You can also extract the call ID from this object, which you'll need for certain API calls (for example to start a recording).
|
|
38
|
+
*/
|
|
39
|
+
data: CallMetadata;
|
|
40
|
+
private readonly subscriber: RTCPeerConnection;
|
|
41
|
+
private readonly publisher: Publisher;
|
|
42
|
+
private readonly trackSubscriptionsSubject = new Subject<
|
|
43
|
+
TrackSubscriptionDetails[]
|
|
44
|
+
>();
|
|
45
|
+
|
|
46
|
+
private statsReporter: StatsReporter;
|
|
47
|
+
private joined$ = new BehaviorSubject<boolean>(false);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Don't call the constructor directly, use the [`StreamVideoClient.joinCall`](./StreamVideoClient.md/#joincall) method to construct a `Call` instance.
|
|
51
|
+
* @param client
|
|
52
|
+
* @param data
|
|
53
|
+
* @param options
|
|
54
|
+
* @param stateStore
|
|
55
|
+
* @param userBatcher
|
|
56
|
+
*/
|
|
57
|
+
constructor(
|
|
58
|
+
data: CallMetadata,
|
|
59
|
+
private readonly client: StreamSfuClient,
|
|
60
|
+
private readonly options: CallOptions,
|
|
61
|
+
private readonly stateStore: StreamVideoWriteableStateStore,
|
|
62
|
+
private readonly userBatcher: Batcher<string>,
|
|
63
|
+
) {
|
|
64
|
+
this.data = data;
|
|
65
|
+
this.subscriber = createSubscriber({
|
|
66
|
+
rpcClient: this.client,
|
|
67
|
+
connectionConfig: this.options.connectionConfig,
|
|
68
|
+
onTrack: this.handleOnTrack,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
this.publisher = new Publisher({
|
|
72
|
+
rpcClient: this.client,
|
|
73
|
+
connectionConfig: this.options.connectionConfig,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
this.statsReporter = createStatsReporter({
|
|
77
|
+
subscriber: this.subscriber,
|
|
78
|
+
publisher: this.publisher,
|
|
79
|
+
store: stateStore,
|
|
80
|
+
edgeName: this.options.edgeName,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const { dispatcher } = this.client;
|
|
84
|
+
registerEventHandlers(this, this.stateStore, dispatcher, this.userBatcher);
|
|
85
|
+
|
|
86
|
+
this.trackSubscriptionsSubject
|
|
87
|
+
.pipe(debounceTime(1200))
|
|
88
|
+
.subscribe((subscriptions) =>
|
|
89
|
+
this.client.updateSubscriptions(subscriptions),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* You can subscribe to WebSocket events provided by the API. To remove a subscription, call the `off` method.
|
|
95
|
+
* Please note that subscribing to WebSocket events is an advanced use-case, for most use-cases it should be enough to watch for changes in the [reactive state store](./StreamVideoClient.md/#readonlystatestore).
|
|
96
|
+
* @param eventName
|
|
97
|
+
* @param fn
|
|
98
|
+
* @returns
|
|
99
|
+
*/
|
|
100
|
+
on = (eventName: string, fn: SfuEventListener) => {
|
|
101
|
+
return this.client.dispatcher.on(eventName, fn);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Remove subscription for WebSocket events that were created by the `on` method.
|
|
106
|
+
* @param eventName
|
|
107
|
+
* @param fn
|
|
108
|
+
* @returns
|
|
109
|
+
*/
|
|
110
|
+
off = (eventName: string, fn: SfuEventListener) => {
|
|
111
|
+
return this.client.dispatcher.off(eventName, fn);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Leave the call and stop the media streams that were published by the call.
|
|
116
|
+
*/
|
|
117
|
+
leave = () => {
|
|
118
|
+
if (!this.joined$.getValue()) {
|
|
119
|
+
throw new Error('Cannot leave call that has already been left.');
|
|
120
|
+
}
|
|
121
|
+
this.joined$.next(false);
|
|
122
|
+
|
|
123
|
+
this.statsReporter.stop();
|
|
124
|
+
this.subscriber.close();
|
|
125
|
+
this.userBatcher.clearBatch();
|
|
126
|
+
|
|
127
|
+
this.publisher.stopPublishing();
|
|
128
|
+
this.client.close();
|
|
129
|
+
|
|
130
|
+
this.stateStore.setCurrentValue(
|
|
131
|
+
this.stateStore.activeCallSubject,
|
|
132
|
+
undefined,
|
|
133
|
+
);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Will initiate a call session with the server and return the call state. Don't call this method directly, use the [`StreamVideoClient.joinCall`](./StreamVideoClient.md/#joincall) method that takes care of this operation.
|
|
138
|
+
*
|
|
139
|
+
* If the join was successful the [`activeCall$` state variable](./StreamVideClient/#readonlystatestore) will be set
|
|
140
|
+
*
|
|
141
|
+
* @returns a promise which resolves once the call join-flow has finished.
|
|
142
|
+
*/
|
|
143
|
+
join = async () => {
|
|
144
|
+
if (this.joined$.getValue()) {
|
|
145
|
+
throw new Error(`Illegal State: Already joined.`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const joinResponseReady = new Promise<CallState | undefined>(
|
|
149
|
+
async (resolve) => {
|
|
150
|
+
this.client.dispatcher.on('joinResponse', (event) => {
|
|
151
|
+
if (event.eventPayload.oneofKind !== 'joinResponse') return;
|
|
152
|
+
|
|
153
|
+
const { callState } = event.eventPayload.joinResponse;
|
|
154
|
+
const currentParticipants = callState?.participants || [];
|
|
155
|
+
|
|
156
|
+
// get user data from the call envelope (invited participants)
|
|
157
|
+
const { users } = this.data;
|
|
158
|
+
|
|
159
|
+
// request user data for uninvited users
|
|
160
|
+
currentParticipants.forEach((participant) => {
|
|
161
|
+
const userData = users[participant.userId];
|
|
162
|
+
if (!userData) this.userBatcher.addToBatch(participant.userId);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
this.stateStore.setCurrentValue(
|
|
166
|
+
this.stateStore.participantsSubject,
|
|
167
|
+
currentParticipants.map<StreamVideoParticipant>((participant) => ({
|
|
168
|
+
...participant,
|
|
169
|
+
isLoggedInUser: participant.sessionId === this.client.sessionId,
|
|
170
|
+
user: users[participant.userId],
|
|
171
|
+
})),
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
this.client.keepAlive();
|
|
175
|
+
this.joined$.next(true);
|
|
176
|
+
resolve(callState); // expose call state
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const genericSdp = await getGenericSdp('recvonly');
|
|
180
|
+
await this.client.join({
|
|
181
|
+
subscriberSdp: genericSdp || '',
|
|
182
|
+
});
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
return joinResponseReady;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Starts publishing the given video stream to the call.
|
|
191
|
+
* The stream will be stopped if the user changes an input device, or if the user leaves the call.
|
|
192
|
+
*
|
|
193
|
+
* If the method was successful the [`activeCall$` state variable](./StreamVideClient/#readonlystatestore) will be cleared
|
|
194
|
+
*
|
|
195
|
+
* Consecutive calls to this method will replace the previously published stream.
|
|
196
|
+
* The previous video stream will be stopped.
|
|
197
|
+
*
|
|
198
|
+
* @angular It's recommended to use the [`InCallDeviceManagerService`](./InCallDeviceManagerService.md) that takes care of this operation for you.
|
|
199
|
+
*
|
|
200
|
+
* @param videoStream the video stream to publish.
|
|
201
|
+
* @param opts the options to use when publishing the stream.
|
|
202
|
+
*/
|
|
203
|
+
publishVideoStream = async (
|
|
204
|
+
videoStream: MediaStream,
|
|
205
|
+
opts: PublishOptions = {},
|
|
206
|
+
) => {
|
|
207
|
+
// we should wait until we get a JoinResponse from the SFU,
|
|
208
|
+
// otherwise we risk breaking the ICETrickle flow.
|
|
209
|
+
await this.assertCallJoined();
|
|
210
|
+
const [videoTrack] = videoStream.getVideoTracks();
|
|
211
|
+
if (!videoTrack) {
|
|
212
|
+
return console.error(`There is no video track in the stream.`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const trackType = TrackType.VIDEO;
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
await this.publisher.publishStream(
|
|
219
|
+
videoStream,
|
|
220
|
+
videoTrack,
|
|
221
|
+
trackType,
|
|
222
|
+
opts,
|
|
223
|
+
);
|
|
224
|
+
await this.client.updateMuteState(trackType, false);
|
|
225
|
+
} catch (e) {
|
|
226
|
+
throw e;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
this.stateStore.updateParticipant(this.client.sessionId, (p) => ({
|
|
230
|
+
videoStream,
|
|
231
|
+
videoDeviceId: videoTrack.getSettings().deviceId,
|
|
232
|
+
publishedTracks: p.publishedTracks.includes(trackType)
|
|
233
|
+
? p.publishedTracks
|
|
234
|
+
: [...p.publishedTracks, trackType],
|
|
235
|
+
}));
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Starts publishing the given audio stream to the call.
|
|
240
|
+
* The stream will be stopped if the user changes an input device, or if the user leaves the call.
|
|
241
|
+
*
|
|
242
|
+
* Consecutive calls to this method will replace the audio stream that is currently being published.
|
|
243
|
+
* The previous audio stream will be stopped.
|
|
244
|
+
*
|
|
245
|
+
* @angular It's recommended to use the [`InCallDeviceManagerService`](./InCallDeviceManagerService.md) that takes care of this operation for you.
|
|
246
|
+
*
|
|
247
|
+
* @param audioStream the audio stream to publish.
|
|
248
|
+
*/
|
|
249
|
+
publishAudioStream = async (audioStream: MediaStream) => {
|
|
250
|
+
// we should wait until we get a JoinResponse from the SFU,
|
|
251
|
+
// otherwise we risk breaking the ICETrickle flow.
|
|
252
|
+
await this.assertCallJoined();
|
|
253
|
+
const [audioTrack] = audioStream.getAudioTracks();
|
|
254
|
+
if (!audioTrack) {
|
|
255
|
+
return console.error(`There is no audio track in the stream`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const trackType = TrackType.AUDIO;
|
|
259
|
+
try {
|
|
260
|
+
await this.publisher.publishStream(audioStream, audioTrack, trackType);
|
|
261
|
+
await this.client.updateMuteState(trackType, false);
|
|
262
|
+
} catch (e) {
|
|
263
|
+
throw e;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
this.stateStore.updateParticipant(this.client.sessionId, (p) => ({
|
|
267
|
+
audioStream,
|
|
268
|
+
audioDeviceId: audioTrack.getSettings().deviceId,
|
|
269
|
+
publishedTracks: p.publishedTracks.includes(trackType)
|
|
270
|
+
? p.publishedTracks
|
|
271
|
+
: [...p.publishedTracks, trackType],
|
|
272
|
+
}));
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Starts publishing the given screen-share stream to the call.
|
|
277
|
+
*
|
|
278
|
+
* Consecutive calls to this method will replace the previous screen-share stream.
|
|
279
|
+
* The previous screen-share stream will be stopped.
|
|
280
|
+
*
|
|
281
|
+
* @angular It's recommended to use the [`InCallDeviceManagerService`](./InCallDeviceManagerService.md) that takes care of this operation for you.
|
|
282
|
+
*
|
|
283
|
+
* @param screenShareStream the screen-share stream to publish.
|
|
284
|
+
*/
|
|
285
|
+
publishScreenShareStream = async (screenShareStream: MediaStream) => {
|
|
286
|
+
// we should wait until we get a JoinResponse from the SFU,
|
|
287
|
+
// otherwise we risk breaking the ICETrickle flow.
|
|
288
|
+
await this.assertCallJoined();
|
|
289
|
+
const [screenShareTrack] = screenShareStream.getVideoTracks();
|
|
290
|
+
if (!screenShareTrack) {
|
|
291
|
+
return console.error(`There is no video track in the stream`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// fires when browser's native 'Stop Sharing button' is clicked
|
|
295
|
+
const onTrackEnded = () => this.stopPublish(trackType);
|
|
296
|
+
screenShareTrack.addEventListener('ended', onTrackEnded);
|
|
297
|
+
|
|
298
|
+
const trackType = TrackType.SCREEN_SHARE;
|
|
299
|
+
try {
|
|
300
|
+
await this.publisher.publishStream(
|
|
301
|
+
screenShareStream,
|
|
302
|
+
screenShareTrack,
|
|
303
|
+
trackType,
|
|
304
|
+
);
|
|
305
|
+
await this.client.updateMuteState(trackType, false);
|
|
306
|
+
} catch (e) {
|
|
307
|
+
throw e;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
this.stateStore.updateParticipant(this.client.sessionId, (p) => ({
|
|
311
|
+
screenShareStream,
|
|
312
|
+
publishedTracks: p.publishedTracks.includes(trackType)
|
|
313
|
+
? p.publishedTracks
|
|
314
|
+
: [...p.publishedTracks, trackType],
|
|
315
|
+
}));
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Stops publishing the given track type to the call, if it is currently being published.
|
|
320
|
+
* Underlying track will be stopped and removed from the publisher.
|
|
321
|
+
*
|
|
322
|
+
* The `audioDeviceId`/`videoDeviceId` property of the [`localParticipant$`](./StreamVideoClient.md/#readonlystatestore) won't be updated, you can do that by calling the [`setAudioDevice`](#setaudiodevice)/[`setVideoDevice`](#setvideodevice) method.
|
|
323
|
+
*
|
|
324
|
+
* @angular It's recommended to use the [`InCallDeviceManagerService`](./InCallDeviceManagerService.md) that takes care of this operation for you.
|
|
325
|
+
*
|
|
326
|
+
* @param trackType the track type to stop publishing.
|
|
327
|
+
*/
|
|
328
|
+
stopPublish = async (trackType: TrackType) => {
|
|
329
|
+
console.log(`stopPublish`, TrackType[trackType]);
|
|
330
|
+
const wasPublishing = this.publisher.unpublishStream(trackType);
|
|
331
|
+
if (wasPublishing) {
|
|
332
|
+
await this.client.updateMuteState(trackType, true);
|
|
333
|
+
|
|
334
|
+
const audioOrVideoOrScreenShareStream =
|
|
335
|
+
trackTypeToParticipantStreamKey(trackType);
|
|
336
|
+
if (audioOrVideoOrScreenShareStream) {
|
|
337
|
+
this.stateStore.updateParticipant(this.client.sessionId, (p) => ({
|
|
338
|
+
publishedTracks: p.publishedTracks.filter((t) => t !== trackType),
|
|
339
|
+
[audioOrVideoOrScreenShareStream]: undefined,
|
|
340
|
+
}));
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Update track subscription configuration for one or more participants.
|
|
347
|
+
* You have to create a subscription for each participant for all the different kinds of tracks you want to receive.
|
|
348
|
+
* You can only subscribe for tracks after the participant started publishing the given kind of track.
|
|
349
|
+
*
|
|
350
|
+
* @param kind the kind of subscription to update.
|
|
351
|
+
* @param changes the list of subscription changes to do.
|
|
352
|
+
*/
|
|
353
|
+
updateSubscriptionsPartial = (
|
|
354
|
+
kind: 'video' | 'screen',
|
|
355
|
+
changes: SubscriptionChanges,
|
|
356
|
+
) => {
|
|
357
|
+
const participants = this.stateStore.updateParticipants(
|
|
358
|
+
Object.entries(changes).reduce<StreamVideoParticipantPatches>(
|
|
359
|
+
(acc, [sessionId, change]) => {
|
|
360
|
+
const prop: keyof StreamVideoParticipant | undefined =
|
|
361
|
+
kind === 'video'
|
|
362
|
+
? 'videoDimension'
|
|
363
|
+
: kind === 'screen'
|
|
364
|
+
? 'screenShareDimension'
|
|
365
|
+
: undefined;
|
|
366
|
+
if (prop) {
|
|
367
|
+
acc[sessionId] = {
|
|
368
|
+
[prop]: change.dimension,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
return acc;
|
|
372
|
+
},
|
|
373
|
+
{},
|
|
374
|
+
),
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
if (participants) {
|
|
378
|
+
this.updateSubscriptions(participants);
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
private updateSubscriptions = (participants: StreamVideoParticipant[]) => {
|
|
383
|
+
const subscriptions: TrackSubscriptionDetails[] = [];
|
|
384
|
+
participants.forEach((p) => {
|
|
385
|
+
if (p.isLoggedInUser) return;
|
|
386
|
+
if (p.videoDimension && p.publishedTracks.includes(TrackType.VIDEO)) {
|
|
387
|
+
subscriptions.push({
|
|
388
|
+
userId: p.userId,
|
|
389
|
+
sessionId: p.sessionId,
|
|
390
|
+
trackType: TrackType.VIDEO,
|
|
391
|
+
dimension: p.videoDimension,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
if (p.publishedTracks.includes(TrackType.AUDIO)) {
|
|
395
|
+
subscriptions.push({
|
|
396
|
+
userId: p.userId,
|
|
397
|
+
sessionId: p.sessionId,
|
|
398
|
+
trackType: TrackType.AUDIO,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
if (
|
|
402
|
+
p.screenShareDimension &&
|
|
403
|
+
p.publishedTracks.includes(TrackType.SCREEN_SHARE)
|
|
404
|
+
) {
|
|
405
|
+
subscriptions.push({
|
|
406
|
+
userId: p.userId,
|
|
407
|
+
sessionId: p.sessionId,
|
|
408
|
+
trackType: TrackType.SCREEN_SHARE,
|
|
409
|
+
dimension: p.screenShareDimension,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
// schedule update
|
|
414
|
+
this.trackSubscriptionsSubject.next(subscriptions);
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* @deprecated use the `callStatsReport$` state [store variable](./StreamVideoClient.md/#readonlystatestore) instead
|
|
419
|
+
* @param kind
|
|
420
|
+
* @param selector
|
|
421
|
+
* @returns
|
|
422
|
+
*/
|
|
423
|
+
getStats = async (
|
|
424
|
+
kind: 'subscriber' | 'publisher',
|
|
425
|
+
selector?: MediaStreamTrack,
|
|
426
|
+
) => {
|
|
427
|
+
return this.statsReporter.getRawStatsForTrack(kind, selector);
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Will enhance the reported stats with additional participant-specific information (`callStatsReport$` state [store variable](./StreamVideoClient.md/#readonlystatestore)).
|
|
432
|
+
* This is usually helpful when detailed stats for a specific participant are needed.
|
|
433
|
+
*
|
|
434
|
+
* @param sessionId the sessionId to start reporting for.
|
|
435
|
+
*/
|
|
436
|
+
startReportingStatsFor = (sessionId: string) => {
|
|
437
|
+
return this.statsReporter.startReportingStatsFor(sessionId);
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Opposite of `startReportingStatsFor`.
|
|
442
|
+
* Will turn off stats reporting for a specific participant.
|
|
443
|
+
*
|
|
444
|
+
* @param sessionId the sessionId to stop reporting for.
|
|
445
|
+
*/
|
|
446
|
+
stopReportingStatsFor = (sessionId: string) => {
|
|
447
|
+
return this.statsReporter.stopReportingStatsFor(sessionId);
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Sets the used audio output device (`audioOutputDeviceId` of the [`localParticipant$`](./StreamVideoClient.md/#readonlystatestore).
|
|
452
|
+
*
|
|
453
|
+
* This method only stores the selection, if you're using custom UI components, you'll have to implement the audio switching, for more information see: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/sinkId.
|
|
454
|
+
*
|
|
455
|
+
* @angular It's recommended to use the [`InCallDeviceManagerService`](./InCallDeviceManagerService.md) that takes care of this operation for you.
|
|
456
|
+
*
|
|
457
|
+
* @param deviceId the selected device, `undefined` means the user wants to use the system's default audio output
|
|
458
|
+
*/
|
|
459
|
+
setAudioOutputDevice = (deviceId?: string) => {
|
|
460
|
+
this.stateStore.updateParticipant(this.client.sessionId, {
|
|
461
|
+
audioOutputDeviceId: deviceId,
|
|
462
|
+
});
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Sets the `audioDeviceId` property of the [`localParticipant$`](./StreamVideoClient.md/#readonlystatestore)).
|
|
467
|
+
*
|
|
468
|
+
* This method only stores the selection, if you want to start publishing a media stream call the [`publishAudioStream` method](#publishaudiostream) that will set `audioDeviceId` as well.
|
|
469
|
+
*
|
|
470
|
+
* @angular It's recommended to use the [`InCallDeviceManagerService`](./InCallDeviceManagerService.md) that takes care of this operation for you.
|
|
471
|
+
*
|
|
472
|
+
* @param deviceId the selected device, pass `undefined` to clear the device selection
|
|
473
|
+
*/
|
|
474
|
+
setAudioDevice = (deviceId?: string) => {
|
|
475
|
+
this.stateStore.updateParticipant(this.client.sessionId, {
|
|
476
|
+
audioDeviceId: deviceId,
|
|
477
|
+
});
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Sets the `videoDeviceId` property of the [`localParticipant$`](./StreamVideoClient.md/#readonlystatestore).
|
|
482
|
+
*
|
|
483
|
+
* This method only stores the selection, if you want to start publishing a media stream call the [`publishVideoStream` method](#publishvideostream) that will set `videoDeviceId` as well.
|
|
484
|
+
*
|
|
485
|
+
* @angular It's recommended to use the [`InCallDeviceManagerService`](./InCallDeviceManagerService.md) that takes care of this operation for you.
|
|
486
|
+
*
|
|
487
|
+
* @param deviceId the selected device, pass `undefined` to clear the device selection
|
|
488
|
+
*/
|
|
489
|
+
setVideoDevice = (deviceId?: string) => {
|
|
490
|
+
this.stateStore.updateParticipant(this.client.sessionId, {
|
|
491
|
+
videoDeviceId: deviceId,
|
|
492
|
+
});
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* @internal
|
|
497
|
+
* @param enabledRids
|
|
498
|
+
* @returns
|
|
499
|
+
*/
|
|
500
|
+
updatePublishQuality = async (enabledRids: string[]) => {
|
|
501
|
+
this.publisher.updateVideoPublishQuality(enabledRids);
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
private get participants() {
|
|
505
|
+
return this.stateStore.getCurrentValue(this.stateStore.participantsSubject);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
private handleOnTrack = (e: RTCTrackEvent) => {
|
|
509
|
+
const [primaryStream] = e.streams;
|
|
510
|
+
// example: `e3f6aaf8-b03d-4911-be36-83f47d37a76a:TRACK_TYPE_VIDEO`
|
|
511
|
+
const [trackId, trackType] = primaryStream.id.split(':');
|
|
512
|
+
console.log(`Got remote ${trackType} track:`, e.track);
|
|
513
|
+
const participantToUpdate = this.participants.find(
|
|
514
|
+
(p) => p.trackLookupPrefix === trackId,
|
|
515
|
+
);
|
|
516
|
+
if (!participantToUpdate) {
|
|
517
|
+
console.error('Received track for unknown participant', trackId, e);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
e.track.addEventListener('mute', () => {
|
|
522
|
+
console.log(
|
|
523
|
+
`Track muted:`,
|
|
524
|
+
participantToUpdate.userId,
|
|
525
|
+
`${trackType}:${trackId}`,
|
|
526
|
+
e.track,
|
|
527
|
+
);
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
e.track.addEventListener('unmute', () => {
|
|
531
|
+
console.log(
|
|
532
|
+
`Track unmuted:`,
|
|
533
|
+
participantToUpdate.userId,
|
|
534
|
+
`${trackType}:${trackId}`,
|
|
535
|
+
e.track,
|
|
536
|
+
);
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
e.track.addEventListener('ended', () => {
|
|
540
|
+
console.log(
|
|
541
|
+
`Track ended:`,
|
|
542
|
+
participantToUpdate.userId,
|
|
543
|
+
`${trackType}:${trackId}`,
|
|
544
|
+
e.track,
|
|
545
|
+
);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
const streamKindProp = (
|
|
549
|
+
{
|
|
550
|
+
TRACK_TYPE_AUDIO: 'audioStream',
|
|
551
|
+
TRACK_TYPE_VIDEO: 'videoStream',
|
|
552
|
+
TRACK_TYPE_SCREEN_SHARE: 'screenShareStream',
|
|
553
|
+
} as const
|
|
554
|
+
)[trackType];
|
|
555
|
+
|
|
556
|
+
if (!streamKindProp) {
|
|
557
|
+
console.error('Unknown track type', trackType);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
const previousStream = participantToUpdate[streamKindProp];
|
|
561
|
+
if (previousStream) {
|
|
562
|
+
console.log(`Cleaning up previous remote tracks`, e.track.kind);
|
|
563
|
+
previousStream.getTracks().forEach((t) => {
|
|
564
|
+
t.stop();
|
|
565
|
+
previousStream.removeTrack(t);
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
this.stateStore.updateParticipant(participantToUpdate.sessionId, {
|
|
569
|
+
[streamKindProp]: primaryStream,
|
|
570
|
+
});
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
private assertCallJoined = () => {
|
|
574
|
+
return new Promise<void>((resolve) => {
|
|
575
|
+
this.joined$
|
|
576
|
+
.pipe(
|
|
577
|
+
takeWhile((isJoined) => !isJoined, true),
|
|
578
|
+
filter((isJoined) => isJoined),
|
|
579
|
+
)
|
|
580
|
+
.subscribe(() => {
|
|
581
|
+
resolve();
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
};
|
|
585
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CallResponse,
|
|
3
|
+
MemberResponse,
|
|
4
|
+
UserResponse,
|
|
5
|
+
} from '../gen/coordinator';
|
|
6
|
+
|
|
7
|
+
export type UserResponseMap = {
|
|
8
|
+
[userId: string]: UserResponse;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export class CallMetadata {
|
|
12
|
+
call: CallResponse;
|
|
13
|
+
users: UserResponseMap;
|
|
14
|
+
|
|
15
|
+
constructor(call: CallResponse, members?: MemberResponse[]) {
|
|
16
|
+
this.call = call;
|
|
17
|
+
this.users =
|
|
18
|
+
members?.reduce<UserResponseMap>((acc, member) => {
|
|
19
|
+
const user = member.user;
|
|
20
|
+
acc[user.id!] = user;
|
|
21
|
+
return acc;
|
|
22
|
+
}, {}) ?? {};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { SfuEvent } from '../gen/video/sfu/event/events';
|
|
2
|
+
|
|
3
|
+
export type SfuEventListener = (event: SfuEvent) => void;
|
|
4
|
+
|
|
5
|
+
export class Dispatcher {
|
|
6
|
+
private subscribers: {
|
|
7
|
+
[eventName: string]: SfuEventListener[] | undefined;
|
|
8
|
+
} = {};
|
|
9
|
+
|
|
10
|
+
dispatch = (message: SfuEvent) => {
|
|
11
|
+
const eventKind = message.eventPayload.oneofKind;
|
|
12
|
+
if (eventKind) {
|
|
13
|
+
// @ts-ignore
|
|
14
|
+
console.log(`Dispatching`, eventKind, message.eventPayload[eventKind]);
|
|
15
|
+
const listeners = this.subscribers[eventKind];
|
|
16
|
+
listeners?.forEach((fn) => {
|
|
17
|
+
try {
|
|
18
|
+
fn(message);
|
|
19
|
+
} catch (e) {
|
|
20
|
+
console.warn(`Listener failed with error`, e);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
on = (eventName: string, fn: SfuEventListener) => {
|
|
27
|
+
(this.subscribers[eventName] ??= []).push(fn);
|
|
28
|
+
return () => {
|
|
29
|
+
this.off(eventName, fn);
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
off = (eventName: string, fn: SfuEventListener) => {
|
|
34
|
+
this.subscribers[eventName] = (this.subscribers[eventName] || []).filter(
|
|
35
|
+
(f) => f !== fn,
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
offAll = (eventName?: string) => {
|
|
40
|
+
if (eventName) {
|
|
41
|
+
this.subscribers[eventName] = [];
|
|
42
|
+
} else {
|
|
43
|
+
this.subscribers = {};
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ReplaySubject } from 'rxjs';
|
|
2
|
+
import { ICETrickle, PeerType } from '../gen/video/sfu/models/models';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A buffer for ICE Candidates. Used for ICE Trickle:
|
|
6
|
+
* - https://bloggeek.me/webrtcglossary/trickle-ice/
|
|
7
|
+
*/
|
|
8
|
+
export class IceTrickleBuffer {
|
|
9
|
+
readonly subscriberCandidates = new ReplaySubject<ICETrickle>();
|
|
10
|
+
readonly publisherCandidates = new ReplaySubject<ICETrickle>();
|
|
11
|
+
|
|
12
|
+
push = (iceTrickle: ICETrickle) => {
|
|
13
|
+
if (iceTrickle.peerType === PeerType.SUBSCRIBER) {
|
|
14
|
+
this.subscriberCandidates.next(iceTrickle);
|
|
15
|
+
} else if (iceTrickle.peerType === PeerType.PUBLISHER_UNSPECIFIED) {
|
|
16
|
+
this.publisherCandidates.next(iceTrickle);
|
|
17
|
+
} else {
|
|
18
|
+
console.warn(`ICETrickle, Unknown peer type`, iceTrickle);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|