@stream-io/video-client 0.2.3 → 0.3.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.
- package/CHANGELOG.md +18 -0
- package/dist/index.browser.es.js +982 -675
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +984 -673
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +982 -675
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +21 -9
- package/dist/src/StreamVideoClient.d.ts +3 -1
- package/dist/src/devices/CameraManager.d.ts +31 -0
- package/dist/src/devices/CameraManagerState.d.ts +28 -0
- package/dist/src/devices/InputMediaDeviceManager.d.ts +47 -0
- package/dist/src/devices/InputMediaDeviceManagerState.d.ts +69 -0
- package/dist/src/devices/MicrophoneManager.d.ts +19 -0
- package/dist/src/devices/MicrophoneManagerState.d.ts +4 -0
- package/dist/src/devices/__tests__/mocks.d.ts +13 -0
- package/dist/src/devices/index.d.ts +4 -0
- package/dist/src/events/call-permissions.d.ts +0 -5
- package/dist/src/events/call.d.ts +0 -6
- package/dist/src/events/index.d.ts +0 -6
- package/dist/src/rtc/Dispatcher.d.ts +2 -2
- package/dist/src/rtc/Publisher.d.ts +0 -1
- package/dist/src/store/CallState.d.ts +164 -89
- package/dist/src/types.d.ts +5 -7
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/Call.ts +130 -44
- package/src/StreamVideoClient.ts +14 -17
- package/src/__tests__/StreamVideoClient.test.ts +3 -0
- package/src/devices/CameraManager.ts +73 -0
- package/src/devices/CameraManagerState.ts +61 -0
- package/src/devices/InputMediaDeviceManager.ts +121 -0
- package/src/devices/InputMediaDeviceManagerState.ts +111 -0
- package/src/devices/MicrophoneManager.ts +45 -0
- package/src/devices/MicrophoneManagerState.ts +9 -0
- package/src/devices/__tests__/CameraManager.test.ts +150 -0
- package/src/devices/__tests__/InputMediaDeviceManager.test.ts +159 -0
- package/src/devices/__tests__/MicrophoneManager.test.ts +103 -0
- package/src/devices/__tests__/mocks.ts +98 -0
- package/src/devices/index.ts +4 -0
- package/src/events/__tests__/call-permissions.test.ts +1 -61
- package/src/events/__tests__/call.test.ts +5 -50
- package/src/events/call-permissions.ts +0 -14
- package/src/events/call.ts +5 -16
- package/src/events/callEventHandlers.ts +2 -57
- package/src/events/index.ts +0 -6
- package/src/rtc/Dispatcher.ts +2 -2
- package/src/rtc/Publisher.ts +4 -6
- package/src/store/CallState.ts +475 -119
- package/src/store/__tests__/CallState.test.ts +447 -1
- package/src/types.ts +4 -8
- package/dist/src/events/__tests__/sessions.test.d.ts +0 -1
- package/dist/src/events/backstage.d.ts +0 -6
- package/dist/src/events/members.d.ts +0 -18
- package/dist/src/events/moderation.d.ts +0 -14
- package/dist/src/events/reactions.d.ts +0 -8
- package/dist/src/events/recording.d.ts +0 -18
- package/dist/src/events/sessions.d.ts +0 -26
- package/src/events/__tests__/backstage.test.ts +0 -15
- package/src/events/__tests__/members.test.ts +0 -135
- package/src/events/__tests__/recording.test.ts +0 -65
- package/src/events/__tests__/sessions.test.ts +0 -135
- package/src/events/backstage.ts +0 -15
- package/src/events/members.ts +0 -62
- package/src/events/moderation.ts +0 -35
- package/src/events/reactions.ts +0 -30
- package/src/events/recording.ts +0 -64
- package/src/events/sessions.ts +0 -102
- /package/dist/src/{events/__tests__/backstage.test.d.ts → devices/__tests__/CameraManager.test.d.ts} +0 -0
- /package/dist/src/{events/__tests__/members.test.d.ts → devices/__tests__/InputMediaDeviceManager.test.d.ts} +0 -0
- /package/dist/src/{events/__tests__/recording.test.d.ts → devices/__tests__/MicrophoneManager.test.d.ts} +0 -0
package/src/Call.ts
CHANGED
|
@@ -9,7 +9,11 @@ import {
|
|
|
9
9
|
Subscriber,
|
|
10
10
|
} from './rtc';
|
|
11
11
|
import { muteTypeToTrackType } from './rtc/helpers/tracks';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
GoAwayReason,
|
|
14
|
+
SdkType,
|
|
15
|
+
TrackType,
|
|
16
|
+
} from './gen/video/sfu/models/models';
|
|
13
17
|
import {
|
|
14
18
|
registerEventHandlers,
|
|
15
19
|
registerRingingCallEventHandlers,
|
|
@@ -105,11 +109,15 @@ import {
|
|
|
105
109
|
CallEventHandler,
|
|
106
110
|
CallEventTypes,
|
|
107
111
|
EventHandler,
|
|
112
|
+
EventTypes,
|
|
108
113
|
Logger,
|
|
109
114
|
StreamCallEvent,
|
|
110
115
|
} from './coordinator/connection/types';
|
|
111
|
-
import { getClientDetails } from './client-details';
|
|
116
|
+
import { getClientDetails, getSdkInfo } from './client-details';
|
|
112
117
|
import { getLogger } from './logger';
|
|
118
|
+
import { CameraManager } from './devices/CameraManager';
|
|
119
|
+
import { MicrophoneManager } from './devices/MicrophoneManager';
|
|
120
|
+
import { CameraDirection } from './devices/CameraManagerState';
|
|
113
121
|
|
|
114
122
|
/**
|
|
115
123
|
* An object representation of a `Call`.
|
|
@@ -146,6 +154,16 @@ export class Call {
|
|
|
146
154
|
*/
|
|
147
155
|
watching: boolean;
|
|
148
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Device manager for the camera
|
|
159
|
+
*/
|
|
160
|
+
readonly camera: CameraManager;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Device manager for the microhpone
|
|
164
|
+
*/
|
|
165
|
+
readonly microphone: MicrophoneManager;
|
|
166
|
+
|
|
149
167
|
/**
|
|
150
168
|
* Flag telling whether this call is a "ringing" call.
|
|
151
169
|
*/
|
|
@@ -174,7 +192,7 @@ export class Call {
|
|
|
174
192
|
private dropTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
175
193
|
|
|
176
194
|
private readonly clientStore: StreamVideoWriteableStateStore;
|
|
177
|
-
|
|
195
|
+
public readonly streamClient: StreamClient;
|
|
178
196
|
private sfuClient?: StreamSfuClient;
|
|
179
197
|
private reconnectAttempts = 0;
|
|
180
198
|
private maxReconnectAttempts = 10;
|
|
@@ -200,7 +218,6 @@ export class Call {
|
|
|
200
218
|
type,
|
|
201
219
|
id,
|
|
202
220
|
streamClient,
|
|
203
|
-
metadata,
|
|
204
221
|
members,
|
|
205
222
|
ownCapabilities,
|
|
206
223
|
sortParticipantsBy,
|
|
@@ -225,13 +242,17 @@ export class Call {
|
|
|
225
242
|
this.state.setSortParticipantsBy(participantSorter);
|
|
226
243
|
}
|
|
227
244
|
|
|
228
|
-
this.state.setMetadata(metadata);
|
|
229
245
|
this.state.setMembers(members || []);
|
|
230
246
|
this.state.setOwnCapabilities(ownCapabilities || []);
|
|
231
247
|
this.state.setCallingState(
|
|
232
248
|
ringing ? CallingState.RINGING : CallingState.IDLE,
|
|
233
249
|
);
|
|
234
250
|
|
|
251
|
+
this.on('all', (event) => {
|
|
252
|
+
// update state with the latest event data
|
|
253
|
+
this.state.updateFromEvent(event);
|
|
254
|
+
});
|
|
255
|
+
|
|
235
256
|
this.leaveCallHooks.push(
|
|
236
257
|
registerEventHandlers(this, this.state, this.dispatcher),
|
|
237
258
|
);
|
|
@@ -246,14 +267,17 @@ export class Call {
|
|
|
246
267
|
(subscriptions) => this.sfuClient?.updateSubscriptions(subscriptions),
|
|
247
268
|
),
|
|
248
269
|
);
|
|
270
|
+
|
|
271
|
+
this.camera = new CameraManager(this);
|
|
272
|
+
this.microphone = new MicrophoneManager(this);
|
|
249
273
|
}
|
|
250
274
|
|
|
251
275
|
private registerEffects() {
|
|
252
276
|
this.leaveCallHooks.push(
|
|
253
|
-
// handles updating the permissions context when the
|
|
254
|
-
createSubscription(this.state.
|
|
255
|
-
if (!
|
|
256
|
-
this.permissionsContext.setCallSettings(
|
|
277
|
+
// handles updating the permissions context when the settings change.
|
|
278
|
+
createSubscription(this.state.settings$, (settings) => {
|
|
279
|
+
if (!settings) return;
|
|
280
|
+
this.permissionsContext.setCallSettings(settings);
|
|
257
281
|
}),
|
|
258
282
|
|
|
259
283
|
// handle the case when the user permissions are modified.
|
|
@@ -284,13 +308,10 @@ export class Call {
|
|
|
284
308
|
}),
|
|
285
309
|
|
|
286
310
|
// handles the case when the user is blocked by the call owner.
|
|
287
|
-
createSubscription(this.state.
|
|
288
|
-
if (!
|
|
311
|
+
createSubscription(this.state.blockedUserIds$, async (blockedUserIds) => {
|
|
312
|
+
if (!blockedUserIds) return;
|
|
289
313
|
const currentUserId = this.currentUserId;
|
|
290
|
-
if (
|
|
291
|
-
currentUserId &&
|
|
292
|
-
metadata.blocked_user_ids.includes(currentUserId)
|
|
293
|
-
) {
|
|
314
|
+
if (currentUserId && blockedUserIds.includes(currentUserId)) {
|
|
294
315
|
this.logger('info', 'Leaving call because of being blocked');
|
|
295
316
|
await this.leave();
|
|
296
317
|
}
|
|
@@ -329,9 +350,9 @@ export class Call {
|
|
|
329
350
|
* @returns a function which can be called to unsubscribe from the given event(s)
|
|
330
351
|
*/
|
|
331
352
|
on(eventName: SfuEventKinds, fn: SfuEventListener): () => void;
|
|
332
|
-
on(eventName:
|
|
353
|
+
on(eventName: EventTypes, fn: CallEventHandler): () => void;
|
|
333
354
|
on(
|
|
334
|
-
eventName: SfuEventKinds |
|
|
355
|
+
eventName: SfuEventKinds | EventTypes,
|
|
335
356
|
fn: SfuEventListener | CallEventHandler,
|
|
336
357
|
) {
|
|
337
358
|
if (isSfuEvent(eventName)) {
|
|
@@ -420,13 +441,6 @@ export class Call {
|
|
|
420
441
|
this.state.setCallingState(CallingState.LEFT);
|
|
421
442
|
};
|
|
422
443
|
|
|
423
|
-
/**
|
|
424
|
-
* A getter for the call metadata.
|
|
425
|
-
*/
|
|
426
|
-
get data() {
|
|
427
|
-
return this.state.metadata;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
444
|
/**
|
|
431
445
|
* A flag indicating whether the call is "ringing" type of call.
|
|
432
446
|
*/
|
|
@@ -445,7 +459,7 @@ export class Call {
|
|
|
445
459
|
* A flag indicating whether the call was created by the current user.
|
|
446
460
|
*/
|
|
447
461
|
get isCreatedByMe() {
|
|
448
|
-
return this.state.
|
|
462
|
+
return this.state.createdBy?.id === this.currentUserId;
|
|
449
463
|
}
|
|
450
464
|
|
|
451
465
|
/**
|
|
@@ -469,7 +483,7 @@ export class Call {
|
|
|
469
483
|
this.ringingSubject.next(true);
|
|
470
484
|
}
|
|
471
485
|
|
|
472
|
-
this.state.
|
|
486
|
+
this.state.updateFromCallResponse(response.call);
|
|
473
487
|
this.state.setMembers(response.members);
|
|
474
488
|
this.state.setOwnCapabilities(response.own_capabilities);
|
|
475
489
|
|
|
@@ -496,7 +510,7 @@ export class Call {
|
|
|
496
510
|
this.ringingSubject.next(true);
|
|
497
511
|
}
|
|
498
512
|
|
|
499
|
-
this.state.
|
|
513
|
+
this.state.updateFromCallResponse(response.call);
|
|
500
514
|
this.state.setMembers(response.members);
|
|
501
515
|
this.state.setOwnCapabilities(response.own_capabilities);
|
|
502
516
|
|
|
@@ -598,7 +612,7 @@ export class Call {
|
|
|
598
612
|
let connectionConfig: RTCConfiguration | undefined;
|
|
599
613
|
try {
|
|
600
614
|
const call = await join(this.streamClient, this.type, this.id, data);
|
|
601
|
-
this.state.
|
|
615
|
+
this.state.updateFromCallResponse(call.metadata);
|
|
602
616
|
this.state.setMembers(call.members);
|
|
603
617
|
this.state.setOwnCapabilities(call.ownCapabilities);
|
|
604
618
|
connectionConfig = call.connectionConfig;
|
|
@@ -815,7 +829,7 @@ export class Call {
|
|
|
815
829
|
});
|
|
816
830
|
}
|
|
817
831
|
|
|
818
|
-
const audioSettings = this.
|
|
832
|
+
const audioSettings = this.state.settings?.audio;
|
|
819
833
|
const isDtxEnabled = !!audioSettings?.opus_dtx_enabled;
|
|
820
834
|
const isRedEnabled = !!audioSettings?.redundant_coding_enabled;
|
|
821
835
|
|
|
@@ -910,6 +924,12 @@ export class Call {
|
|
|
910
924
|
this.reconnectAttempts = 0; // reset the reconnect attempts counter
|
|
911
925
|
this.state.setCallingState(CallingState.JOINED);
|
|
912
926
|
|
|
927
|
+
// React uses a different device management for now
|
|
928
|
+
if (getSdkInfo()?.type !== SdkType.REACT) {
|
|
929
|
+
this.initCamera();
|
|
930
|
+
this.initMic();
|
|
931
|
+
}
|
|
932
|
+
|
|
913
933
|
// 3. once we have the "joinResponse", and possibly reconciled the local state
|
|
914
934
|
// we schedule a fast subscription update for all remote participants
|
|
915
935
|
// that were visible before we reconnected or migrated to a new SFU.
|
|
@@ -1196,6 +1216,8 @@ export class Call {
|
|
|
1196
1216
|
*
|
|
1197
1217
|
*
|
|
1198
1218
|
* @param deviceId the selected device, pass `undefined` to clear the device selection
|
|
1219
|
+
*
|
|
1220
|
+
* @deprecated use call.microphone.select
|
|
1199
1221
|
*/
|
|
1200
1222
|
setAudioDevice = (deviceId?: string) => {
|
|
1201
1223
|
if (!this.sfuClient) return;
|
|
@@ -1210,6 +1232,8 @@ export class Call {
|
|
|
1210
1232
|
* 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.
|
|
1211
1233
|
*
|
|
1212
1234
|
* @param deviceId the selected device, pass `undefined` to clear the device selection
|
|
1235
|
+
*
|
|
1236
|
+
* @deprecated use call.camera.select
|
|
1213
1237
|
*/
|
|
1214
1238
|
setVideoDevice = (deviceId?: string) => {
|
|
1215
1239
|
if (!this.sfuClient) return;
|
|
@@ -1516,7 +1540,7 @@ export class Call {
|
|
|
1516
1540
|
>(`${this.streamClientBasePath}`, updates);
|
|
1517
1541
|
|
|
1518
1542
|
const { call, members, own_capabilities } = response;
|
|
1519
|
-
this.state.
|
|
1543
|
+
this.state.updateFromCallResponse(call);
|
|
1520
1544
|
this.state.setMembers(members);
|
|
1521
1545
|
this.state.setOwnCapabilities(own_capabilities);
|
|
1522
1546
|
|
|
@@ -1617,23 +1641,23 @@ export class Call {
|
|
|
1617
1641
|
|
|
1618
1642
|
private scheduleAutoDrop = () => {
|
|
1619
1643
|
if (this.dropTimeout) clearTimeout(this.dropTimeout);
|
|
1620
|
-
const subscription = this.state.
|
|
1644
|
+
const subscription = this.state.settings$
|
|
1621
1645
|
.pipe(
|
|
1622
1646
|
pairwise(),
|
|
1623
|
-
tap(([
|
|
1624
|
-
if (!
|
|
1647
|
+
tap(([prevSettings, currentSettings]) => {
|
|
1648
|
+
if (!currentSettings || !this.clientStore.connectedUser) return;
|
|
1625
1649
|
|
|
1626
1650
|
const isOutgoingCall =
|
|
1627
|
-
this.currentUserId ===
|
|
1651
|
+
this.currentUserId === this.state.createdBy?.id;
|
|
1628
1652
|
|
|
1629
1653
|
const [prevTimeoutMs, timeoutMs] = isOutgoingCall
|
|
1630
1654
|
? [
|
|
1631
|
-
|
|
1632
|
-
|
|
1655
|
+
prevSettings?.ring.auto_cancel_timeout_ms,
|
|
1656
|
+
currentSettings.ring.auto_cancel_timeout_ms,
|
|
1633
1657
|
]
|
|
1634
1658
|
: [
|
|
1635
|
-
|
|
1636
|
-
|
|
1659
|
+
prevSettings?.ring.incoming_call_timeout_ms,
|
|
1660
|
+
currentSettings.ring.incoming_call_timeout_ms,
|
|
1637
1661
|
];
|
|
1638
1662
|
if (
|
|
1639
1663
|
typeof timeoutMs === 'undefined' ||
|
|
@@ -1658,7 +1682,6 @@ export class Call {
|
|
|
1658
1682
|
|
|
1659
1683
|
/**
|
|
1660
1684
|
* Retrieves the list of recordings for the current call or call session.
|
|
1661
|
-
* Updates the call state with the returned array of CallRecording objects.
|
|
1662
1685
|
*
|
|
1663
1686
|
* If `callSessionId` is provided, it will return the recordings for that call session.
|
|
1664
1687
|
* Otherwise, all recordings for the current call will be returned.
|
|
@@ -1672,13 +1695,9 @@ export class Call {
|
|
|
1672
1695
|
if (callSessionId) {
|
|
1673
1696
|
endpoint = `${endpoint}/${callSessionId}`;
|
|
1674
1697
|
}
|
|
1675
|
-
|
|
1698
|
+
return this.streamClient.get<ListRecordingsResponse>(
|
|
1676
1699
|
`${endpoint}/recordings`,
|
|
1677
1700
|
);
|
|
1678
|
-
|
|
1679
|
-
this.state.setCallRecordingsList(response.recordings);
|
|
1680
|
-
|
|
1681
|
-
return response;
|
|
1682
1701
|
};
|
|
1683
1702
|
|
|
1684
1703
|
/**
|
|
@@ -1692,4 +1711,71 @@ export class Call {
|
|
|
1692
1711
|
{ custom: payload },
|
|
1693
1712
|
);
|
|
1694
1713
|
};
|
|
1714
|
+
|
|
1715
|
+
private initCamera() {
|
|
1716
|
+
if (
|
|
1717
|
+
this.state.localParticipant?.videoStream ||
|
|
1718
|
+
!this.permissionsContext.hasPermission('send-video')
|
|
1719
|
+
) {
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
// Set camera direction if it's not yet set
|
|
1724
|
+
// This will also start publishing if camera is enabled
|
|
1725
|
+
if (!this.camera.state.direction && !this.camera.state.selectedDevice) {
|
|
1726
|
+
let defaultDirection: CameraDirection = 'front';
|
|
1727
|
+
const backendSetting = this.state.settings?.video.camera_facing;
|
|
1728
|
+
if (backendSetting) {
|
|
1729
|
+
defaultDirection = backendSetting === 'front' ? 'front' : 'back';
|
|
1730
|
+
}
|
|
1731
|
+
this.camera.selectDirection(defaultDirection);
|
|
1732
|
+
} else if (this.camera.state.status === 'enabled') {
|
|
1733
|
+
// Publish already started media streams (this is the case if there is a lobby screen before join)
|
|
1734
|
+
// Wait for media stream
|
|
1735
|
+
this.camera.state.mediaStream$
|
|
1736
|
+
.pipe(takeWhile((s) => s === undefined, true))
|
|
1737
|
+
.subscribe((stream) => {
|
|
1738
|
+
if (!this.state.localParticipant?.videoStream) {
|
|
1739
|
+
this.publishVideoStream(stream!);
|
|
1740
|
+
}
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
// Apply backend config (this is the case if there is no lobby screen before join)
|
|
1745
|
+
if (
|
|
1746
|
+
this.camera.state.status === undefined &&
|
|
1747
|
+
this.state.settings?.video.camera_default_on
|
|
1748
|
+
) {
|
|
1749
|
+
void this.camera.enable();
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
private initMic() {
|
|
1754
|
+
if (
|
|
1755
|
+
this.state.localParticipant?.audioStream ||
|
|
1756
|
+
!this.permissionsContext.hasPermission('send-audio')
|
|
1757
|
+
) {
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
// Publish already started media streams (this is the case if there is a lobby screen before join)
|
|
1762
|
+
if (this.microphone.state.status === 'enabled') {
|
|
1763
|
+
// Wait for media stream
|
|
1764
|
+
this.microphone.state.mediaStream$
|
|
1765
|
+
.pipe(takeWhile((s) => s === undefined, true))
|
|
1766
|
+
.subscribe((stream) => {
|
|
1767
|
+
if (!this.state.localParticipant?.audioStream) {
|
|
1768
|
+
this.publishAudioStream(stream!);
|
|
1769
|
+
}
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
// Apply backend config (this is the case if there is no lobby screen before join)
|
|
1774
|
+
if (
|
|
1775
|
+
this.microphone.state.status === undefined &&
|
|
1776
|
+
this.state.settings?.audio.mic_default_on
|
|
1777
|
+
) {
|
|
1778
|
+
void this.microphone.enable();
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1695
1781
|
}
|
package/src/StreamVideoClient.ts
CHANGED
|
@@ -217,16 +217,15 @@ export class StreamVideoClient {
|
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
this.logger('info', `New call created and registered: ${call.cid}`);
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
);
|
|
220
|
+
const newCall = new Call({
|
|
221
|
+
streamClient: this.streamClient,
|
|
222
|
+
type: call.type,
|
|
223
|
+
id: call.id,
|
|
224
|
+
members,
|
|
225
|
+
clientStore: this.writeableStateStore,
|
|
226
|
+
});
|
|
227
|
+
newCall.state.updateFromCallResponse(call);
|
|
228
|
+
this.writeableStateStore.registerCall(newCall);
|
|
230
229
|
}),
|
|
231
230
|
);
|
|
232
231
|
|
|
@@ -246,7 +245,6 @@ export class StreamVideoClient {
|
|
|
246
245
|
// if `call.created` was received before `call.ring`.
|
|
247
246
|
// In that case, we cleanup the already tracked call.
|
|
248
247
|
const prevCall = this.writeableStateStore.findCall(call.type, call.id);
|
|
249
|
-
const prevMetadata = prevCall?.state.metadata;
|
|
250
248
|
await prevCall?.leave();
|
|
251
249
|
// we create a new call
|
|
252
250
|
const theCall = new Call({
|
|
@@ -256,8 +254,8 @@ export class StreamVideoClient {
|
|
|
256
254
|
members,
|
|
257
255
|
clientStore: this.writeableStateStore,
|
|
258
256
|
ringing: true,
|
|
259
|
-
metadata: prevMetadata,
|
|
260
257
|
});
|
|
258
|
+
theCall.state.updateFromCallResponse(call);
|
|
261
259
|
// we fetch the latest metadata for the call from the server
|
|
262
260
|
await theCall.get();
|
|
263
261
|
this.writeableStateStore.registerCall(theCall);
|
|
@@ -357,12 +355,12 @@ export class StreamVideoClient {
|
|
|
357
355
|
streamClient: this.streamClient,
|
|
358
356
|
id: c.call.id,
|
|
359
357
|
type: c.call.type,
|
|
360
|
-
metadata: c.call,
|
|
361
358
|
members: c.members,
|
|
362
359
|
ownCapabilities: c.own_capabilities,
|
|
363
360
|
watching: data.watch,
|
|
364
361
|
clientStore: this.writeableStateStore,
|
|
365
362
|
});
|
|
363
|
+
call.state.updateFromCallResponse(c.call);
|
|
366
364
|
if (data.watch) {
|
|
367
365
|
this.writeableStateStore.registerCall(call);
|
|
368
366
|
}
|
|
@@ -374,10 +372,9 @@ export class StreamVideoClient {
|
|
|
374
372
|
};
|
|
375
373
|
};
|
|
376
374
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
375
|
+
/**
|
|
376
|
+
* Returns a list of available data centers available for hosting calls.
|
|
377
|
+
*/
|
|
381
378
|
edges = async () => {
|
|
382
379
|
return this.streamClient.get<GetEdgesResponse>(`/edges`);
|
|
383
380
|
};
|
|
@@ -7,6 +7,9 @@ import { StreamVideoServerClient } from '../StreamVideoServerClient';
|
|
|
7
7
|
const apiKey = process.env.STREAM_API_KEY!;
|
|
8
8
|
const secret = process.env.STREAM_SECRET!;
|
|
9
9
|
|
|
10
|
+
vi.mock('../devices/CameraManager.ts');
|
|
11
|
+
vi.mock('../devices/MicrophoneManager.ts');
|
|
12
|
+
|
|
10
13
|
const tokenProvider = (userId: string) => {
|
|
11
14
|
const serverClient = new StreamVideoServerClient(apiKey, { secret });
|
|
12
15
|
return async () => {
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Observable } from 'rxjs';
|
|
2
|
+
import { Call } from '../Call';
|
|
3
|
+
import { CameraDirection, CameraManagerState } from './CameraManagerState';
|
|
4
|
+
import { InputMediaDeviceManager } from './InputMediaDeviceManager';
|
|
5
|
+
import { getVideoDevices, getVideoStream } from './devices';
|
|
6
|
+
import { TrackType } from '../gen/video/sfu/models/models';
|
|
7
|
+
|
|
8
|
+
export class CameraManager extends InputMediaDeviceManager<CameraManagerState> {
|
|
9
|
+
constructor(call: Call) {
|
|
10
|
+
super(call, new CameraManagerState());
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Select the camera direaction
|
|
15
|
+
* @param direction
|
|
16
|
+
*/
|
|
17
|
+
async selectDirection(direction: Exclude<CameraDirection, undefined>) {
|
|
18
|
+
this.state.setDirection(direction);
|
|
19
|
+
// Providing both device id and direction doesn't work, so we deselect the device
|
|
20
|
+
this.state.setDevice(undefined);
|
|
21
|
+
await this.applySettingsToStream();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Flips the camera direction: if it's front it will change to back, if it's back, it will change to front.
|
|
26
|
+
*
|
|
27
|
+
* Note: if there is no available camera with the desired direction, this method will do nothing.
|
|
28
|
+
* @returns
|
|
29
|
+
*/
|
|
30
|
+
async flip() {
|
|
31
|
+
const newDirection = this.state.direction === 'front' ? 'back' : 'front';
|
|
32
|
+
this.selectDirection(newDirection);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
protected getDevices(): Observable<MediaDeviceInfo[]> {
|
|
36
|
+
return getVideoDevices();
|
|
37
|
+
}
|
|
38
|
+
protected getStream(
|
|
39
|
+
constraints: MediaTrackConstraints,
|
|
40
|
+
): Promise<MediaStream> {
|
|
41
|
+
// We can't set both device id and facing mode
|
|
42
|
+
// Device id has higher priority
|
|
43
|
+
if (!constraints.deviceId && this.state.direction) {
|
|
44
|
+
constraints.facingMode =
|
|
45
|
+
this.state.direction === 'front' ? 'user' : 'environment';
|
|
46
|
+
}
|
|
47
|
+
return getVideoStream(constraints);
|
|
48
|
+
}
|
|
49
|
+
protected publishStream(stream: MediaStream): Promise<void> {
|
|
50
|
+
return this.call.publishVideoStream(stream);
|
|
51
|
+
}
|
|
52
|
+
protected stopPublishStream(): Promise<void> {
|
|
53
|
+
return this.call.stopPublish(TrackType.VIDEO);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Disables the video tracks of the camera
|
|
58
|
+
*/
|
|
59
|
+
pause() {
|
|
60
|
+
this.state.mediaStream?.getVideoTracks().forEach((track) => {
|
|
61
|
+
track.enabled = false;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* (Re)enables the video tracks of the camera
|
|
67
|
+
*/
|
|
68
|
+
resume() {
|
|
69
|
+
this.state.mediaStream?.getVideoTracks().forEach((track) => {
|
|
70
|
+
track.enabled = true;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { BehaviorSubject, Observable, distinctUntilChanged } from 'rxjs';
|
|
2
|
+
import { InputMediaDeviceManagerState } from './InputMediaDeviceManagerState';
|
|
3
|
+
import { isReactNative } from '../helpers/platforms';
|
|
4
|
+
|
|
5
|
+
export type CameraDirection = 'front' | 'back' | undefined;
|
|
6
|
+
|
|
7
|
+
export class CameraManagerState extends InputMediaDeviceManagerState {
|
|
8
|
+
private directionSubject = new BehaviorSubject<CameraDirection>(undefined);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Observable that emits the preferred camera direction
|
|
12
|
+
* front - means the camera facing the user
|
|
13
|
+
* back - means the camera facing the environment
|
|
14
|
+
*/
|
|
15
|
+
direction$: Observable<CameraDirection>;
|
|
16
|
+
|
|
17
|
+
constructor() {
|
|
18
|
+
super();
|
|
19
|
+
this.direction$ = this.directionSubject
|
|
20
|
+
.asObservable()
|
|
21
|
+
.pipe(distinctUntilChanged());
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The preferred camera direction
|
|
26
|
+
* front - means the camera facing the user
|
|
27
|
+
* back - means the camera facing the environment
|
|
28
|
+
*/
|
|
29
|
+
get direction() {
|
|
30
|
+
return this.getCurrentValue(this.direction$);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
36
|
+
setDirection(direction: CameraDirection) {
|
|
37
|
+
this.setCurrentValue(this.directionSubject, direction);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @internal
|
|
42
|
+
*/
|
|
43
|
+
setMediaStream(stream: MediaStream | undefined): void {
|
|
44
|
+
super.setMediaStream(stream);
|
|
45
|
+
if (stream) {
|
|
46
|
+
// RN getSettings() doesn't return facingMode, so we don't verify camera direction
|
|
47
|
+
const direction = isReactNative()
|
|
48
|
+
? this.direction
|
|
49
|
+
: stream.getVideoTracks()[0]?.getSettings().facingMode === 'environment'
|
|
50
|
+
? 'back'
|
|
51
|
+
: 'front';
|
|
52
|
+
this.setDirection(direction);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
protected getDeviceIdFromStream(stream: MediaStream): string | undefined {
|
|
57
|
+
return stream.getVideoTracks()[0]?.getSettings().deviceId as
|
|
58
|
+
| string
|
|
59
|
+
| undefined;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Observable } from 'rxjs';
|
|
2
|
+
import { Call } from '../Call';
|
|
3
|
+
import { CallingState } from '../store';
|
|
4
|
+
import { InputMediaDeviceManagerState } from './InputMediaDeviceManagerState';
|
|
5
|
+
import { disposeOfMediaStream } from './devices';
|
|
6
|
+
import { isReactNative } from '../helpers/platforms';
|
|
7
|
+
|
|
8
|
+
export abstract class InputMediaDeviceManager<
|
|
9
|
+
T extends InputMediaDeviceManagerState,
|
|
10
|
+
> {
|
|
11
|
+
constructor(protected readonly call: Call, public readonly state: T) {}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Lists the available audio/video devices
|
|
15
|
+
*
|
|
16
|
+
* Note: It prompts the user for a permission to use devices (if not already granted)
|
|
17
|
+
*
|
|
18
|
+
* @returns an Observable that will be updated if a device is connected or disconnected
|
|
19
|
+
*/
|
|
20
|
+
listDevices() {
|
|
21
|
+
return this.getDevices();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Starts camera/microphone
|
|
26
|
+
*/
|
|
27
|
+
async enable() {
|
|
28
|
+
if (this.state.status === 'enabled') {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
await this.startStream();
|
|
32
|
+
this.state.setStatus('enabled');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Stops camera/microphone
|
|
37
|
+
* @returns
|
|
38
|
+
*/
|
|
39
|
+
async disable() {
|
|
40
|
+
if (this.state.status === 'disabled') {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
await this.stopStream();
|
|
44
|
+
this.state.setStatus('disabled');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* If current device statis is disabled, it will enable the device, else it will disable it.
|
|
49
|
+
* @returns
|
|
50
|
+
*/
|
|
51
|
+
async toggle() {
|
|
52
|
+
if (this.state.status === 'enabled') {
|
|
53
|
+
return this.disable();
|
|
54
|
+
} else {
|
|
55
|
+
return this.enable();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Select device
|
|
61
|
+
*
|
|
62
|
+
* Note: this method is not supported in React Native
|
|
63
|
+
*
|
|
64
|
+
* @param deviceId
|
|
65
|
+
*/
|
|
66
|
+
async select(deviceId: string | undefined) {
|
|
67
|
+
if (isReactNative()) {
|
|
68
|
+
throw new Error('This method is not supported in React Native');
|
|
69
|
+
}
|
|
70
|
+
if (deviceId === this.state.selectedDevice) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
this.state.setDevice(deviceId);
|
|
74
|
+
await this.applySettingsToStream();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
protected async applySettingsToStream() {
|
|
78
|
+
if (this.state.status === 'enabled') {
|
|
79
|
+
await this.stopStream();
|
|
80
|
+
await this.startStream();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
abstract pause(): void;
|
|
85
|
+
|
|
86
|
+
abstract resume(): void;
|
|
87
|
+
|
|
88
|
+
protected abstract getDevices(): Observable<MediaDeviceInfo[]>;
|
|
89
|
+
|
|
90
|
+
protected abstract getStream(
|
|
91
|
+
constraints: MediaTrackConstraints,
|
|
92
|
+
): Promise<MediaStream>;
|
|
93
|
+
|
|
94
|
+
protected abstract publishStream(stream: MediaStream): Promise<void>;
|
|
95
|
+
|
|
96
|
+
protected abstract stopPublishStream(): Promise<void>;
|
|
97
|
+
|
|
98
|
+
private async stopStream() {
|
|
99
|
+
if (!this.state.mediaStream) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (this.call.state.callingState === CallingState.JOINED) {
|
|
103
|
+
await this.stopPublishStream();
|
|
104
|
+
} else if (this.state.mediaStream) {
|
|
105
|
+
disposeOfMediaStream(this.state.mediaStream);
|
|
106
|
+
}
|
|
107
|
+
this.state.setMediaStream(undefined);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private async startStream() {
|
|
111
|
+
if (this.state.mediaStream) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const constraints = { deviceId: this.state.selectedDevice };
|
|
115
|
+
const stream = await this.getStream(constraints);
|
|
116
|
+
if (this.call.state.callingState === CallingState.JOINED) {
|
|
117
|
+
await this.publishStream(stream);
|
|
118
|
+
}
|
|
119
|
+
this.state.setMediaStream(stream);
|
|
120
|
+
}
|
|
121
|
+
}
|