@stream-io/video-client 1.13.1 → 1.15.0
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 +14 -0
- package/dist/index.browser.es.js +1704 -1762
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +1706 -1780
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +1704 -1762
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +61 -30
- package/dist/src/StreamSfuClient.d.ts +4 -5
- package/dist/src/devices/CameraManager.d.ts +5 -8
- package/dist/src/devices/InputMediaDeviceManager.d.ts +5 -5
- package/dist/src/devices/MicrophoneManager.d.ts +7 -2
- package/dist/src/devices/ScreenShareManager.d.ts +1 -2
- package/dist/src/gen/coordinator/index.d.ts +904 -515
- package/dist/src/gen/video/sfu/event/events.d.ts +38 -19
- package/dist/src/gen/video/sfu/models/models.d.ts +76 -9
- package/dist/src/helpers/array.d.ts +7 -0
- package/dist/src/permissions/PermissionsContext.d.ts +6 -0
- package/dist/src/rtc/BasePeerConnection.d.ts +90 -0
- package/dist/src/rtc/Dispatcher.d.ts +0 -1
- package/dist/src/rtc/IceTrickleBuffer.d.ts +3 -2
- package/dist/src/rtc/Publisher.d.ts +32 -86
- package/dist/src/rtc/Subscriber.d.ts +4 -56
- package/dist/src/rtc/TransceiverCache.d.ts +55 -0
- package/dist/src/rtc/codecs.d.ts +1 -15
- package/dist/src/rtc/helpers/sdp.d.ts +8 -0
- package/dist/src/rtc/helpers/tracks.d.ts +1 -0
- package/dist/src/rtc/index.d.ts +3 -0
- package/dist/src/rtc/videoLayers.d.ts +11 -25
- package/dist/src/stats/{stateStoreStatsReporter.d.ts → CallStateStatsReporter.d.ts} +5 -1
- package/dist/src/stats/SfuStatsReporter.d.ts +4 -2
- package/dist/src/stats/index.d.ts +1 -1
- package/dist/src/stats/types.d.ts +8 -0
- package/dist/src/store/CallState.d.ts +47 -5
- package/dist/src/store/rxUtils.d.ts +15 -1
- package/dist/src/types.d.ts +26 -22
- package/package.json +1 -1
- package/src/Call.ts +310 -271
- package/src/StreamSfuClient.ts +9 -14
- package/src/StreamVideoClient.ts +1 -1
- package/src/__tests__/Call.publishing.test.ts +306 -0
- package/src/devices/CameraManager.ts +33 -16
- package/src/devices/InputMediaDeviceManager.ts +36 -27
- package/src/devices/MicrophoneManager.ts +29 -8
- package/src/devices/ScreenShareManager.ts +6 -8
- package/src/devices/__tests__/CameraManager.test.ts +111 -14
- package/src/devices/__tests__/InputMediaDeviceManager.test.ts +4 -4
- package/src/devices/__tests__/MicrophoneManager.test.ts +59 -21
- package/src/devices/__tests__/ScreenShareManager.test.ts +5 -5
- package/src/devices/__tests__/mocks.ts +1 -0
- package/src/events/__tests__/internal.test.ts +132 -0
- package/src/events/__tests__/mutes.test.ts +0 -3
- package/src/events/__tests__/speaker.test.ts +92 -0
- package/src/events/participant.ts +3 -4
- package/src/gen/coordinator/index.ts +902 -514
- package/src/gen/video/sfu/event/events.ts +91 -30
- package/src/gen/video/sfu/models/models.ts +105 -13
- package/src/helpers/array.ts +14 -0
- package/src/permissions/PermissionsContext.ts +22 -0
- package/src/permissions/__tests__/PermissionsContext.test.ts +40 -0
- package/src/rpc/__tests__/createClient.test.ts +38 -0
- package/src/rpc/createClient.ts +11 -5
- package/src/rtc/BasePeerConnection.ts +240 -0
- package/src/rtc/Dispatcher.ts +0 -9
- package/src/rtc/IceTrickleBuffer.ts +24 -4
- package/src/rtc/Publisher.ts +210 -528
- package/src/rtc/Subscriber.ts +26 -200
- package/src/rtc/TransceiverCache.ts +120 -0
- package/src/rtc/__tests__/Publisher.test.ts +407 -210
- package/src/rtc/__tests__/Subscriber.test.ts +88 -36
- package/src/rtc/__tests__/mocks/webrtc.mocks.ts +22 -2
- package/src/rtc/__tests__/videoLayers.test.ts +161 -54
- package/src/rtc/codecs.ts +1 -131
- package/src/rtc/helpers/__tests__/rtcConfiguration.test.ts +34 -0
- package/src/rtc/helpers/__tests__/sdp.test.ts +59 -0
- package/src/rtc/helpers/sdp.ts +30 -0
- package/src/rtc/helpers/tracks.ts +3 -0
- package/src/rtc/index.ts +4 -0
- package/src/rtc/videoLayers.ts +68 -76
- package/src/stats/{stateStoreStatsReporter.ts → CallStateStatsReporter.ts} +58 -27
- package/src/stats/SfuStatsReporter.ts +31 -3
- package/src/stats/index.ts +1 -1
- package/src/stats/types.ts +12 -0
- package/src/store/CallState.ts +115 -5
- package/src/store/__tests__/CallState.test.ts +101 -0
- package/src/store/rxUtils.ts +23 -1
- package/src/types.ts +27 -22
- package/dist/src/helpers/sdp-munging.d.ts +0 -24
- package/dist/src/rtc/bitrateLookup.d.ts +0 -2
- package/dist/src/rtc/helpers/iceCandidate.d.ts +0 -2
- package/src/helpers/__tests__/hq-audio-sdp.ts +0 -332
- package/src/helpers/__tests__/sdp-munging.test.ts +0 -283
- package/src/helpers/sdp-munging.ts +0 -265
- package/src/rtc/__tests__/bitrateLookup.test.ts +0 -12
- package/src/rtc/__tests__/codecs.test.ts +0 -145
- package/src/rtc/bitrateLookup.ts +0 -61
- package/src/rtc/helpers/iceCandidate.ts +0 -16
- /package/dist/src/{compatibility.d.ts → helpers/compatibility.d.ts} +0 -0
- /package/src/{compatibility.ts → helpers/compatibility.ts} +0 -0
package/src/StreamSfuClient.ts
CHANGED
|
@@ -20,19 +20,19 @@ import {
|
|
|
20
20
|
SendAnswerRequest,
|
|
21
21
|
SendStatsRequest,
|
|
22
22
|
SetPublisherRequest,
|
|
23
|
+
TrackMuteState,
|
|
23
24
|
TrackSubscriptionDetails,
|
|
24
|
-
UpdateMuteStatesRequest,
|
|
25
25
|
} from './gen/video/sfu/signal_rpc/signal';
|
|
26
|
-
import { ICETrickle
|
|
26
|
+
import { ICETrickle } from './gen/video/sfu/models/models';
|
|
27
27
|
import { StreamClient } from './coordinator/connection/client';
|
|
28
28
|
import { generateUUIDv4 } from './coordinator/connection/utils';
|
|
29
29
|
import { Credentials } from './gen/coordinator';
|
|
30
30
|
import { Logger } from './coordinator/connection/types';
|
|
31
31
|
import { getLogger, getLogLevel } from './logger';
|
|
32
32
|
import {
|
|
33
|
-
promiseWithResolvers,
|
|
34
|
-
PromiseWithResolvers,
|
|
35
33
|
makeSafePromise,
|
|
34
|
+
PromiseWithResolvers,
|
|
35
|
+
promiseWithResolvers,
|
|
36
36
|
SafePromise,
|
|
37
37
|
} from './helpers/promise';
|
|
38
38
|
import { getTimers } from './timers';
|
|
@@ -277,7 +277,7 @@ export class StreamSfuClient {
|
|
|
277
277
|
this.dispose();
|
|
278
278
|
};
|
|
279
279
|
|
|
280
|
-
dispose = () => {
|
|
280
|
+
private dispose = () => {
|
|
281
281
|
this.logger('debug', 'Disposing SFU client');
|
|
282
282
|
this.unsubscribeIceTrickle();
|
|
283
283
|
this.unsubscribeNetworkChanged();
|
|
@@ -286,6 +286,7 @@ export class StreamSfuClient {
|
|
|
286
286
|
clearTimeout(this.migrateAwayTimeout);
|
|
287
287
|
this.abortController.abort();
|
|
288
288
|
this.migrationTask?.resolve();
|
|
289
|
+
this.iceTrickleBuffer.dispose();
|
|
289
290
|
};
|
|
290
291
|
|
|
291
292
|
leaveAndClose = async (reason: string) => {
|
|
@@ -340,17 +341,11 @@ export class StreamSfuClient {
|
|
|
340
341
|
);
|
|
341
342
|
};
|
|
342
343
|
|
|
343
|
-
|
|
344
|
-
await this.joinTask;
|
|
345
|
-
return this.updateMuteStates({ muteStates: [{ trackType, muted }] });
|
|
346
|
-
};
|
|
347
|
-
|
|
348
|
-
updateMuteStates = async (
|
|
349
|
-
data: Omit<UpdateMuteStatesRequest, 'sessionId'>,
|
|
350
|
-
) => {
|
|
344
|
+
updateMuteStates = async (muteStates: TrackMuteState[]) => {
|
|
351
345
|
await this.joinTask;
|
|
352
346
|
return retryable(
|
|
353
|
-
() =>
|
|
347
|
+
() =>
|
|
348
|
+
this.rpc.updateMuteStates({ muteStates, sessionId: this.sessionId }),
|
|
354
349
|
this.abortController.signal,
|
|
355
350
|
);
|
|
356
351
|
};
|
package/src/StreamVideoClient.ts
CHANGED
|
@@ -415,7 +415,7 @@ export class StreamVideoClient {
|
|
|
415
415
|
clientStore: this.writeableStateStore,
|
|
416
416
|
});
|
|
417
417
|
call.state.updateFromCallResponse(c.call);
|
|
418
|
-
await call.applyDeviceConfig(false);
|
|
418
|
+
await call.applyDeviceConfig(c.call.settings, false);
|
|
419
419
|
if (data.watch) {
|
|
420
420
|
this.writeableStateStore.registerCall(call);
|
|
421
421
|
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import '../rtc/__tests__/mocks/webrtc.mocks';
|
|
2
|
+
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { Call } from '../Call';
|
|
5
|
+
import { Publisher } from '../rtc';
|
|
6
|
+
import { StreamClient } from '../coordinator/connection/client';
|
|
7
|
+
import { generateUUIDv4 } from '../coordinator/connection/utils';
|
|
8
|
+
import { PermissionsContext } from '../permissions';
|
|
9
|
+
import { OwnCapability } from '../gen/coordinator';
|
|
10
|
+
import { StreamVideoWriteableStateStore } from '../store';
|
|
11
|
+
import { TrackType } from '../gen/video/sfu/models/models';
|
|
12
|
+
import { StreamSfuClient } from '../StreamSfuClient';
|
|
13
|
+
|
|
14
|
+
describe('Publishing and Unpublishing tracks', () => {
|
|
15
|
+
let call: Call;
|
|
16
|
+
|
|
17
|
+
beforeEach(async () => {
|
|
18
|
+
call = new Call({
|
|
19
|
+
type: 'test',
|
|
20
|
+
id: generateUUIDv4(),
|
|
21
|
+
streamClient: new StreamClient('abc'),
|
|
22
|
+
clientStore: new StreamVideoWriteableStateStore(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const ctx = new PermissionsContext();
|
|
26
|
+
ctx.setPermissions([
|
|
27
|
+
OwnCapability.SEND_AUDIO,
|
|
28
|
+
OwnCapability.SEND_VIDEO,
|
|
29
|
+
OwnCapability.SCREENSHARE,
|
|
30
|
+
]);
|
|
31
|
+
// @ts-expect-error permissionsContext is private
|
|
32
|
+
call['permissionsContext'] = ctx;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('Validations', () => {
|
|
36
|
+
it('publishing is not allowed only when call is not joined', async () => {
|
|
37
|
+
const ms = new MediaStream();
|
|
38
|
+
const err = 'Call not joined yet.';
|
|
39
|
+
await expect(call.publish(ms, TrackType.VIDEO)).rejects.toThrowError(err);
|
|
40
|
+
await expect(call.publish(ms, TrackType.AUDIO)).rejects.toThrowError(err);
|
|
41
|
+
await expect(
|
|
42
|
+
call.publish(ms, TrackType.SCREEN_SHARE),
|
|
43
|
+
).rejects.toThrowError(err);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('publishing is not allowed when permissions are not set', async () => {
|
|
47
|
+
// @ts-expect-error sfuClient is private
|
|
48
|
+
call['sfuClient'] = {};
|
|
49
|
+
|
|
50
|
+
call['permissionsContext'].setPermissions([]);
|
|
51
|
+
|
|
52
|
+
const ms = new MediaStream();
|
|
53
|
+
await expect(call.publish(ms, TrackType.VIDEO)).rejects.toThrowError(
|
|
54
|
+
`No permission to publish VIDEO`,
|
|
55
|
+
);
|
|
56
|
+
await expect(call.publish(ms, TrackType.AUDIO)).rejects.toThrowError(
|
|
57
|
+
'No permission to publish AUDIO',
|
|
58
|
+
);
|
|
59
|
+
await expect(
|
|
60
|
+
call.publish(ms, TrackType.SCREEN_SHARE),
|
|
61
|
+
).rejects.toThrowError('No permission to publish SCREEN_SHARE');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('publishing is not allowed when the publisher is not initialized', async () => {
|
|
65
|
+
// @ts-expect-error sfuClient is private
|
|
66
|
+
call['sfuClient'] = {};
|
|
67
|
+
|
|
68
|
+
const ms = new MediaStream();
|
|
69
|
+
await expect(call.publish(ms, TrackType.VIDEO)).rejects.toThrowError(
|
|
70
|
+
'Publisher is not initialized',
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('publishing is not allowed when there are no tracks in the stream', async () => {
|
|
75
|
+
// @ts-expect-error sfuClient is private
|
|
76
|
+
call['sfuClient'] = {};
|
|
77
|
+
// @ts-expect-error publisher is private
|
|
78
|
+
call['publisher'] = {};
|
|
79
|
+
|
|
80
|
+
const ms = new MediaStream();
|
|
81
|
+
vi.spyOn(ms, 'getVideoTracks').mockReturnValue([]);
|
|
82
|
+
vi.spyOn(ms, 'getAudioTracks').mockReturnValue([]);
|
|
83
|
+
|
|
84
|
+
await expect(call.publish(ms, TrackType.VIDEO)).rejects.toThrowError(
|
|
85
|
+
'There is no VIDEO track in the stream',
|
|
86
|
+
);
|
|
87
|
+
await expect(call.publish(ms, TrackType.AUDIO)).rejects.toThrowError(
|
|
88
|
+
'There is no AUDIO track in the stream',
|
|
89
|
+
);
|
|
90
|
+
await expect(
|
|
91
|
+
call.publish(ms, TrackType.SCREEN_SHARE),
|
|
92
|
+
).rejects.toThrowError('There is no SCREEN_SHARE track in the stream');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('publishing is not allowed when the track ended', async () => {
|
|
96
|
+
// @ts-expect-error sfuClient is private
|
|
97
|
+
call['sfuClient'] = {};
|
|
98
|
+
// @ts-expect-error publisher is private
|
|
99
|
+
call['publisher'] = {};
|
|
100
|
+
|
|
101
|
+
const ms = new MediaStream();
|
|
102
|
+
const track = new MediaStreamTrack();
|
|
103
|
+
vi.spyOn(ms, 'getVideoTracks').mockReturnValue([track]);
|
|
104
|
+
vi.spyOn(track, 'readyState', 'get').mockReturnValue('ended');
|
|
105
|
+
|
|
106
|
+
await expect(call.publish(ms, TrackType.VIDEO)).rejects.toThrowError(
|
|
107
|
+
`Can't publish ended tracks.`,
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('Publishing and Unpublishing', () => {
|
|
113
|
+
const sessionId = 'abc';
|
|
114
|
+
let publisher: Publisher;
|
|
115
|
+
let sfuClient: StreamSfuClient;
|
|
116
|
+
|
|
117
|
+
beforeEach(() => {
|
|
118
|
+
// @ts-expect-error partial data
|
|
119
|
+
call.state.updateOrAddParticipant(sessionId, {
|
|
120
|
+
sessionId,
|
|
121
|
+
publishedTracks: [],
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
sfuClient = vi.fn() as unknown as StreamSfuClient;
|
|
125
|
+
// @ts-expect-error sessionId is readonly
|
|
126
|
+
sfuClient['sessionId'] = sessionId;
|
|
127
|
+
sfuClient.updateMuteStates = vi.fn();
|
|
128
|
+
|
|
129
|
+
publisher = vi.fn() as unknown as Publisher;
|
|
130
|
+
publisher.publish = vi.fn();
|
|
131
|
+
publisher.stopTracks = vi.fn();
|
|
132
|
+
|
|
133
|
+
call['sfuClient'] = sfuClient;
|
|
134
|
+
call.publisher = publisher;
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('publish video stream', async () => {
|
|
138
|
+
const track = new MediaStreamTrack();
|
|
139
|
+
const mediaStream = new MediaStream();
|
|
140
|
+
vi.spyOn(mediaStream, 'getVideoTracks').mockReturnValue([track]);
|
|
141
|
+
|
|
142
|
+
await call.publish(mediaStream, TrackType.VIDEO);
|
|
143
|
+
expect(publisher.publish).toHaveBeenCalledWith(track, TrackType.VIDEO);
|
|
144
|
+
expect(call['trackPublishOrder']).toEqual([TrackType.VIDEO]);
|
|
145
|
+
|
|
146
|
+
expect(sfuClient.updateMuteStates).toHaveBeenCalledWith([
|
|
147
|
+
{ trackType: TrackType.VIDEO, muted: false },
|
|
148
|
+
]);
|
|
149
|
+
|
|
150
|
+
const participant = call.state.findParticipantBySessionId(sessionId);
|
|
151
|
+
expect(participant).toBeDefined();
|
|
152
|
+
expect(participant!.publishedTracks).toEqual([TrackType.VIDEO]);
|
|
153
|
+
expect(participant!.videoStream).toEqual(mediaStream);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('publish audio stream', async () => {
|
|
157
|
+
const track = new MediaStreamTrack();
|
|
158
|
+
const mediaStream = new MediaStream();
|
|
159
|
+
vi.spyOn(mediaStream, 'getAudioTracks').mockReturnValue([track]);
|
|
160
|
+
|
|
161
|
+
await call.publish(mediaStream, TrackType.AUDIO);
|
|
162
|
+
expect(publisher.publish).toHaveBeenCalledWith(track, TrackType.AUDIO);
|
|
163
|
+
expect(call['trackPublishOrder']).toEqual([TrackType.AUDIO]);
|
|
164
|
+
|
|
165
|
+
expect(sfuClient.updateMuteStates).toHaveBeenCalledWith([
|
|
166
|
+
{ trackType: TrackType.AUDIO, muted: false },
|
|
167
|
+
]);
|
|
168
|
+
|
|
169
|
+
const participant = call.state.findParticipantBySessionId(sessionId);
|
|
170
|
+
expect(participant).toBeDefined();
|
|
171
|
+
expect(participant!.publishedTracks).toEqual([TrackType.AUDIO]);
|
|
172
|
+
expect(participant!.audioStream).toEqual(mediaStream);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('publish screen share stream', async () => {
|
|
176
|
+
const track = new MediaStreamTrack();
|
|
177
|
+
const mediaStream = new MediaStream();
|
|
178
|
+
vi.spyOn(mediaStream, 'getVideoTracks').mockReturnValue([track]);
|
|
179
|
+
|
|
180
|
+
await call.publish(mediaStream, TrackType.SCREEN_SHARE);
|
|
181
|
+
expect(publisher.publish).toHaveBeenCalledWith(
|
|
182
|
+
track,
|
|
183
|
+
TrackType.SCREEN_SHARE,
|
|
184
|
+
);
|
|
185
|
+
expect(call['trackPublishOrder']).toEqual([TrackType.SCREEN_SHARE]);
|
|
186
|
+
|
|
187
|
+
expect(sfuClient.updateMuteStates).toHaveBeenCalledWith([
|
|
188
|
+
{ trackType: TrackType.SCREEN_SHARE, muted: false },
|
|
189
|
+
]);
|
|
190
|
+
|
|
191
|
+
const participant = call.state.findParticipantBySessionId(sessionId);
|
|
192
|
+
expect(participant).toBeDefined();
|
|
193
|
+
expect(participant!.publishedTracks).toEqual([TrackType.SCREEN_SHARE]);
|
|
194
|
+
expect(participant!.screenShareStream).toEqual(mediaStream);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('publish screen share stream with audio', async () => {
|
|
198
|
+
const videoTrack = new MediaStreamTrack();
|
|
199
|
+
const audioTrack = new MediaStreamTrack();
|
|
200
|
+
const mediaStream = new MediaStream();
|
|
201
|
+
vi.spyOn(mediaStream, 'getVideoTracks').mockReturnValue([videoTrack]);
|
|
202
|
+
vi.spyOn(mediaStream, 'getAudioTracks').mockReturnValue([audioTrack]);
|
|
203
|
+
|
|
204
|
+
await call.publish(mediaStream, TrackType.SCREEN_SHARE);
|
|
205
|
+
expect(publisher.publish).toHaveBeenCalledWith(
|
|
206
|
+
videoTrack,
|
|
207
|
+
TrackType.SCREEN_SHARE,
|
|
208
|
+
);
|
|
209
|
+
expect(publisher.publish).toHaveBeenCalledWith(
|
|
210
|
+
audioTrack,
|
|
211
|
+
TrackType.SCREEN_SHARE_AUDIO,
|
|
212
|
+
);
|
|
213
|
+
expect(call['trackPublishOrder']).toEqual([
|
|
214
|
+
TrackType.SCREEN_SHARE,
|
|
215
|
+
TrackType.SCREEN_SHARE_AUDIO,
|
|
216
|
+
]);
|
|
217
|
+
|
|
218
|
+
expect(sfuClient.updateMuteStates).toHaveBeenCalledWith([
|
|
219
|
+
{ trackType: TrackType.SCREEN_SHARE, muted: false },
|
|
220
|
+
{ trackType: TrackType.SCREEN_SHARE_AUDIO, muted: false },
|
|
221
|
+
]);
|
|
222
|
+
|
|
223
|
+
const participant = call.state.findParticipantBySessionId(sessionId);
|
|
224
|
+
expect(participant).toBeDefined();
|
|
225
|
+
expect(participant!.publishedTracks).toEqual([
|
|
226
|
+
TrackType.SCREEN_SHARE,
|
|
227
|
+
TrackType.SCREEN_SHARE_AUDIO,
|
|
228
|
+
]);
|
|
229
|
+
expect(participant!.screenShareStream).toEqual(mediaStream);
|
|
230
|
+
expect(participant!.screenShareAudioStream).toEqual(mediaStream);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('unpublish video stream', async () => {
|
|
234
|
+
const mediaStream = new MediaStream();
|
|
235
|
+
call.state.updateParticipant(sessionId, {
|
|
236
|
+
publishedTracks: [TrackType.VIDEO, TrackType.AUDIO],
|
|
237
|
+
videoStream: mediaStream,
|
|
238
|
+
});
|
|
239
|
+
await call.stopPublish(TrackType.VIDEO);
|
|
240
|
+
expect(publisher.publish).not.toHaveBeenCalled();
|
|
241
|
+
expect(publisher.stopTracks).toHaveBeenCalledWith(TrackType.VIDEO);
|
|
242
|
+
const participant = call.state.findParticipantBySessionId(sessionId);
|
|
243
|
+
expect(participant!.publishedTracks).toEqual([TrackType.AUDIO]);
|
|
244
|
+
expect(participant!.videoStream).toBeUndefined();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('unpublish audio stream', async () => {
|
|
248
|
+
const mediaStream = new MediaStream();
|
|
249
|
+
call.state.updateParticipant(sessionId, {
|
|
250
|
+
publishedTracks: [TrackType.VIDEO, TrackType.AUDIO],
|
|
251
|
+
audioStream: mediaStream,
|
|
252
|
+
});
|
|
253
|
+
await call.stopPublish(TrackType.AUDIO);
|
|
254
|
+
expect(publisher.publish).not.toHaveBeenCalled();
|
|
255
|
+
expect(publisher.stopTracks).toHaveBeenCalledWith(TrackType.AUDIO);
|
|
256
|
+
const participant = call.state.findParticipantBySessionId(sessionId);
|
|
257
|
+
expect(participant!.publishedTracks).toEqual([TrackType.VIDEO]);
|
|
258
|
+
expect(participant!.audioStream).toBeUndefined();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('unpublish screen share stream', async () => {
|
|
262
|
+
const mediaStream = new MediaStream();
|
|
263
|
+
call.state.updateParticipant(sessionId, {
|
|
264
|
+
publishedTracks: [TrackType.SCREEN_SHARE, TrackType.SCREEN_SHARE_AUDIO],
|
|
265
|
+
screenShareStream: mediaStream,
|
|
266
|
+
screenShareAudioStream: mediaStream,
|
|
267
|
+
});
|
|
268
|
+
await call.stopPublish(
|
|
269
|
+
TrackType.SCREEN_SHARE,
|
|
270
|
+
TrackType.SCREEN_SHARE_AUDIO,
|
|
271
|
+
);
|
|
272
|
+
expect(publisher.publish).not.toHaveBeenCalled();
|
|
273
|
+
expect(publisher.stopTracks).toHaveBeenCalledWith(
|
|
274
|
+
TrackType.SCREEN_SHARE,
|
|
275
|
+
TrackType.SCREEN_SHARE_AUDIO,
|
|
276
|
+
);
|
|
277
|
+
const participant = call.state.findParticipantBySessionId(sessionId);
|
|
278
|
+
expect(participant!.publishedTracks).toEqual([]);
|
|
279
|
+
expect(participant!.screenShareStream).toBeUndefined();
|
|
280
|
+
expect(participant!.screenShareAudioStream).toBeUndefined();
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe('Deprecated methods', () => {
|
|
285
|
+
it('publishVideoStream', async () => {
|
|
286
|
+
const ms = new MediaStream();
|
|
287
|
+
call.publish = vi.fn();
|
|
288
|
+
await call.publishVideoStream(ms);
|
|
289
|
+
expect(call.publish).toHaveBeenCalledWith(ms, TrackType.VIDEO);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('publishAudioStream', async () => {
|
|
293
|
+
const ms = new MediaStream();
|
|
294
|
+
call.publish = vi.fn();
|
|
295
|
+
await call.publishAudioStream(ms);
|
|
296
|
+
expect(call.publish).toHaveBeenCalledWith(ms, TrackType.AUDIO);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('publishScreenShareStream', async () => {
|
|
300
|
+
const ms = new MediaStream();
|
|
301
|
+
call.publish = vi.fn();
|
|
302
|
+
await call.publishScreenShareStream(ms);
|
|
303
|
+
expect(call.publish).toHaveBeenCalledWith(ms, TrackType.SCREEN_SHARE);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
});
|
|
@@ -3,9 +3,9 @@ import { Call } from '../Call';
|
|
|
3
3
|
import { CameraDirection, CameraManagerState } from './CameraManagerState';
|
|
4
4
|
import { InputMediaDeviceManager } from './InputMediaDeviceManager';
|
|
5
5
|
import { getVideoDevices, getVideoStream } from './devices';
|
|
6
|
+
import { OwnCapability, VideoSettingsResponse } from '../gen/coordinator';
|
|
6
7
|
import { TrackType } from '../gen/video/sfu/models/models';
|
|
7
|
-
import {
|
|
8
|
-
import { isMobile } from '../compatibility';
|
|
8
|
+
import { isMobile } from '../helpers/compatibility';
|
|
9
9
|
import { isReactNative } from '../helpers/platforms';
|
|
10
10
|
|
|
11
11
|
export class CameraManager extends InputMediaDeviceManager<CameraManagerState> {
|
|
@@ -86,14 +86,39 @@ export class CameraManager extends InputMediaDeviceManager<CameraManagerState> {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
/**
|
|
89
|
-
*
|
|
89
|
+
* Applies the video settings to the camera.
|
|
90
90
|
*
|
|
91
|
-
* @
|
|
92
|
-
* @
|
|
93
|
-
* @param codec the codec to use for encoding the video.
|
|
91
|
+
* @param settings the video settings to apply.
|
|
92
|
+
* @param publish whether to publish the stream after applying the settings.
|
|
94
93
|
*/
|
|
95
|
-
|
|
96
|
-
this.call.
|
|
94
|
+
async apply(settings: VideoSettingsResponse, publish: boolean) {
|
|
95
|
+
const hasPublishedVideo = !!this.call.state.localParticipant?.videoStream;
|
|
96
|
+
const hasPermission = this.call.permissionsContext.hasPermission(
|
|
97
|
+
OwnCapability.SEND_AUDIO,
|
|
98
|
+
);
|
|
99
|
+
if (hasPublishedVideo || !hasPermission) return;
|
|
100
|
+
|
|
101
|
+
// Wait for any in progress camera operation
|
|
102
|
+
await this.statusChangeSettled();
|
|
103
|
+
|
|
104
|
+
const { target_resolution, camera_facing, camera_default_on } = settings;
|
|
105
|
+
await this.selectTargetResolution(target_resolution);
|
|
106
|
+
|
|
107
|
+
// Set camera direction if it's not yet set
|
|
108
|
+
if (!this.state.direction && !this.state.selectedDevice) {
|
|
109
|
+
this.state.setDirection(camera_facing === 'front' ? 'front' : 'back');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!publish) return;
|
|
113
|
+
|
|
114
|
+
const { mediaStream } = this.state;
|
|
115
|
+
if (this.enabled && mediaStream) {
|
|
116
|
+
// The camera is already enabled (e.g. lobby screen). Publish the stream
|
|
117
|
+
await this.publishStream(mediaStream);
|
|
118
|
+
} else if (this.state.status === undefined && camera_default_on) {
|
|
119
|
+
// Start camera if backend config specifies, and there is no local setting
|
|
120
|
+
await this.enable();
|
|
121
|
+
}
|
|
97
122
|
}
|
|
98
123
|
|
|
99
124
|
protected getDevices(): Observable<MediaDeviceInfo[]> {
|
|
@@ -118,12 +143,4 @@ export class CameraManager extends InputMediaDeviceManager<CameraManagerState> {
|
|
|
118
143
|
}
|
|
119
144
|
return getVideoStream(constraints);
|
|
120
145
|
}
|
|
121
|
-
|
|
122
|
-
protected publishStream(stream: MediaStream): Promise<void> {
|
|
123
|
-
return this.call.publishVideoStream(stream);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
protected stopPublishStream(stopTracks: boolean): Promise<void> {
|
|
127
|
-
return this.call.stopPublish(TrackType.VIDEO, stopTracks);
|
|
128
|
-
}
|
|
129
146
|
}
|
|
@@ -241,43 +241,45 @@ export abstract class InputMediaDeviceManager<
|
|
|
241
241
|
|
|
242
242
|
protected abstract getStream(constraints: C): Promise<MediaStream>;
|
|
243
243
|
|
|
244
|
-
protected
|
|
244
|
+
protected publishStream(stream: MediaStream): Promise<void> {
|
|
245
|
+
return this.call.publish(stream, this.trackType);
|
|
246
|
+
}
|
|
245
247
|
|
|
246
|
-
protected
|
|
248
|
+
protected stopPublishStream(): Promise<void> {
|
|
249
|
+
return this.call.stopPublish(this.trackType);
|
|
250
|
+
}
|
|
247
251
|
|
|
248
252
|
protected getTracks(): MediaStreamTrack[] {
|
|
249
253
|
return this.state.mediaStream?.getTracks() ?? [];
|
|
250
254
|
}
|
|
251
255
|
|
|
252
256
|
protected async muteStream(stopTracks: boolean = true) {
|
|
253
|
-
|
|
257
|
+
const mediaStream = this.state.mediaStream;
|
|
258
|
+
if (!mediaStream) return;
|
|
254
259
|
this.logger('debug', `${stopTracks ? 'Stopping' : 'Disabling'} stream`);
|
|
255
260
|
if (this.call.state.callingState === CallingState.JOINED) {
|
|
256
|
-
await this.stopPublishStream(
|
|
261
|
+
await this.stopPublishStream();
|
|
257
262
|
}
|
|
258
263
|
this.muteLocalStream(stopTracks);
|
|
259
264
|
const allEnded = this.getTracks().every((t) => t.readyState === 'ended');
|
|
260
265
|
if (allEnded) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
// @ts-expect-error release() is present in react-native-webrtc
|
|
264
|
-
typeof this.state.mediaStream.release === 'function'
|
|
265
|
-
) {
|
|
266
|
+
// @ts-expect-error release() is present in react-native-webrtc
|
|
267
|
+
if (typeof mediaStream.release === 'function') {
|
|
266
268
|
// @ts-expect-error called to dispose the stream in RN
|
|
267
|
-
|
|
269
|
+
mediaStream.release();
|
|
268
270
|
}
|
|
269
271
|
this.state.setMediaStream(undefined, undefined);
|
|
270
272
|
this.filters.forEach((entry) => entry.stop?.());
|
|
271
273
|
}
|
|
272
274
|
}
|
|
273
275
|
|
|
274
|
-
private
|
|
276
|
+
private disableTracks() {
|
|
275
277
|
this.getTracks().forEach((track) => {
|
|
276
278
|
if (track.enabled) track.enabled = false;
|
|
277
279
|
});
|
|
278
280
|
}
|
|
279
281
|
|
|
280
|
-
private
|
|
282
|
+
private enableTracks() {
|
|
281
283
|
this.getTracks().forEach((track) => {
|
|
282
284
|
if (!track.enabled) track.enabled = true;
|
|
283
285
|
});
|
|
@@ -296,7 +298,7 @@ export abstract class InputMediaDeviceManager<
|
|
|
296
298
|
if (stopTracks) {
|
|
297
299
|
this.stopTracks();
|
|
298
300
|
} else {
|
|
299
|
-
this.
|
|
301
|
+
this.disableTracks();
|
|
300
302
|
}
|
|
301
303
|
}
|
|
302
304
|
|
|
@@ -309,7 +311,7 @@ export abstract class InputMediaDeviceManager<
|
|
|
309
311
|
this.getTracks().every((t) => t.readyState === 'live')
|
|
310
312
|
) {
|
|
311
313
|
stream = this.state.mediaStream;
|
|
312
|
-
this.
|
|
314
|
+
this.enableTracks();
|
|
313
315
|
} else {
|
|
314
316
|
const defaultConstraints = this.state.defaultConstraints;
|
|
315
317
|
const constraints: MediaTrackConstraints = {
|
|
@@ -414,11 +416,22 @@ export abstract class InputMediaDeviceManager<
|
|
|
414
416
|
await this.disable();
|
|
415
417
|
}
|
|
416
418
|
};
|
|
417
|
-
|
|
419
|
+
const createTrackMuteHandler = (muted: boolean) => () => {
|
|
420
|
+
this.call.notifyTrackMuteState(muted, this.trackType).catch((err) => {
|
|
421
|
+
this.logger('warn', 'Error while notifying track mute state', err);
|
|
422
|
+
});
|
|
423
|
+
};
|
|
424
|
+
stream.getTracks().forEach((track) => {
|
|
425
|
+
const muteHandler = createTrackMuteHandler(true);
|
|
426
|
+
const unmuteHandler = createTrackMuteHandler(false);
|
|
427
|
+
track.addEventListener('mute', muteHandler);
|
|
428
|
+
track.addEventListener('unmute', unmuteHandler);
|
|
418
429
|
track.addEventListener('ended', handleTrackEnded);
|
|
419
|
-
this.subscriptions.push(() =>
|
|
420
|
-
track.removeEventListener('
|
|
421
|
-
|
|
430
|
+
this.subscriptions.push(() => {
|
|
431
|
+
track.removeEventListener('mute', muteHandler);
|
|
432
|
+
track.removeEventListener('unmute', unmuteHandler);
|
|
433
|
+
track.removeEventListener('ended', handleTrackEnded);
|
|
434
|
+
});
|
|
422
435
|
});
|
|
423
436
|
}
|
|
424
437
|
}
|
|
@@ -447,11 +460,8 @@ export abstract class InputMediaDeviceManager<
|
|
|
447
460
|
|
|
448
461
|
let isDeviceDisconnected = false;
|
|
449
462
|
let isDeviceReplaced = false;
|
|
450
|
-
const currentDevice = this.
|
|
451
|
-
|
|
452
|
-
deviceId,
|
|
453
|
-
);
|
|
454
|
-
const prevDevice = this.findDeviceInList(prevDevices, deviceId);
|
|
463
|
+
const currentDevice = this.findDevice(currentDevices, deviceId);
|
|
464
|
+
const prevDevice = this.findDevice(prevDevices, deviceId);
|
|
455
465
|
if (!currentDevice && prevDevice) {
|
|
456
466
|
isDeviceDisconnected = true;
|
|
457
467
|
} else if (
|
|
@@ -490,9 +500,8 @@ export abstract class InputMediaDeviceManager<
|
|
|
490
500
|
);
|
|
491
501
|
}
|
|
492
502
|
|
|
493
|
-
private
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
);
|
|
503
|
+
private findDevice(devices: MediaDeviceInfo[], deviceId: string) {
|
|
504
|
+
const kind = this.mediaDeviceKind;
|
|
505
|
+
return devices.find((d) => d.deviceId === deviceId && d.kind === kind);
|
|
497
506
|
}
|
|
498
507
|
}
|
|
@@ -9,6 +9,7 @@ import { TrackType } from '../gen/video/sfu/models/models';
|
|
|
9
9
|
import { createSoundDetector } from '../helpers/sound-detector';
|
|
10
10
|
import { isReactNative } from '../helpers/platforms';
|
|
11
11
|
import {
|
|
12
|
+
AudioSettingsResponse,
|
|
12
13
|
NoiseCancellationSettingsModeEnum,
|
|
13
14
|
OwnCapability,
|
|
14
15
|
} from '../gen/coordinator';
|
|
@@ -201,6 +202,34 @@ export class MicrophoneManager extends InputMediaDeviceManager<MicrophoneManager
|
|
|
201
202
|
await this.stopSpeakingWhileMutedDetection();
|
|
202
203
|
}
|
|
203
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Applies the audio settings to the microphone.
|
|
207
|
+
* @param settings the audio settings to apply.
|
|
208
|
+
* @param publish whether to publish the stream after applying the settings.
|
|
209
|
+
*/
|
|
210
|
+
async apply(settings: AudioSettingsResponse, publish: boolean) {
|
|
211
|
+
if (!publish) return;
|
|
212
|
+
|
|
213
|
+
const hasPublishedAudio = !!this.call.state.localParticipant?.audioStream;
|
|
214
|
+
const hasPermission = this.call.permissionsContext.hasPermission(
|
|
215
|
+
OwnCapability.SEND_AUDIO,
|
|
216
|
+
);
|
|
217
|
+
if (hasPublishedAudio || !hasPermission) return;
|
|
218
|
+
|
|
219
|
+
// Wait for any in progress mic operation
|
|
220
|
+
await this.statusChangeSettled();
|
|
221
|
+
|
|
222
|
+
// Publish media stream that was set before we joined
|
|
223
|
+
const { mediaStream } = this.state;
|
|
224
|
+
if (this.enabled && mediaStream) {
|
|
225
|
+
// The mic is already enabled (e.g. lobby screen). Publish the stream
|
|
226
|
+
await this.publishStream(mediaStream);
|
|
227
|
+
} else if (this.state.status === undefined && settings.mic_default_on) {
|
|
228
|
+
// Start mic if backend config specifies, and there is no local setting
|
|
229
|
+
await this.enable();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
204
233
|
protected getDevices(): Observable<MediaDeviceInfo[]> {
|
|
205
234
|
return getAudioDevices();
|
|
206
235
|
}
|
|
@@ -211,14 +240,6 @@ export class MicrophoneManager extends InputMediaDeviceManager<MicrophoneManager
|
|
|
211
240
|
return getAudioStream(constraints);
|
|
212
241
|
}
|
|
213
242
|
|
|
214
|
-
protected publishStream(stream: MediaStream): Promise<void> {
|
|
215
|
-
return this.call.publishAudioStream(stream);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
protected stopPublishStream(stopTracks: boolean): Promise<void> {
|
|
219
|
-
return this.call.stopPublish(TrackType.AUDIO, stopTracks);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
243
|
private async startSpeakingWhileMutedDetection(deviceId?: string) {
|
|
223
244
|
await withoutConcurrency(this.soundDetectorConcurrencyTag, async () => {
|
|
224
245
|
await this.stopSpeakingWhileMutedDetection();
|
|
@@ -46,7 +46,7 @@ export class ScreenShareManager extends InputMediaDeviceManager<
|
|
|
46
46
|
async disableScreenShareAudio(): Promise<void> {
|
|
47
47
|
this.state.setAudioEnabled(false);
|
|
48
48
|
if (this.call.publisher?.isPublishing(TrackType.SCREEN_SHARE_AUDIO)) {
|
|
49
|
-
await this.call.stopPublish(TrackType.SCREEN_SHARE_AUDIO
|
|
49
|
+
await this.call.stopPublish(TrackType.SCREEN_SHARE_AUDIO);
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -79,13 +79,11 @@ export class ScreenShareManager extends InputMediaDeviceManager<
|
|
|
79
79
|
return getScreenShareStream(constraints);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
protected
|
|
83
|
-
return this.call.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
await this.call.stopPublish(TrackType.SCREEN_SHARE, stopTracks);
|
|
88
|
-
await this.call.stopPublish(TrackType.SCREEN_SHARE_AUDIO, stopTracks);
|
|
82
|
+
protected async stopPublishStream(): Promise<void> {
|
|
83
|
+
return this.call.stopPublish(
|
|
84
|
+
TrackType.SCREEN_SHARE,
|
|
85
|
+
TrackType.SCREEN_SHARE_AUDIO,
|
|
86
|
+
);
|
|
89
87
|
}
|
|
90
88
|
|
|
91
89
|
/**
|