@stream-io/video-client 0.6.6 → 0.6.8
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 +134 -17
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +134 -17
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +134 -17
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +17 -1
- package/dist/src/StreamVideoClient.d.ts +8 -1
- package/dist/src/coordinator/connection/client.d.ts +6 -6
- package/dist/src/devices/InputMediaDeviceManager.d.ts +18 -1
- package/dist/src/devices/SpeakerManager.d.ts +6 -1
- package/dist/src/gen/coordinator/index.d.ts +374 -2
- package/dist/src/rtc/signal.d.ts +1 -0
- package/package.json +4 -4
- package/src/Call.ts +42 -4
- package/src/StreamVideoClient.ts +15 -0
- package/src/devices/InputMediaDeviceManager.ts +140 -48
- package/src/devices/SpeakerManager.ts +6 -1
- package/src/devices/__tests__/InputMediaDeviceManagerFilters.test.ts +115 -0
- package/src/gen/coordinator/index.ts +367 -2
- package/src/helpers/DynascaleManager.ts +0 -2
- package/src/store/CallState.ts +5 -0
package/src/Call.ts
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
GoLiveRequest,
|
|
31
31
|
GoLiveResponse,
|
|
32
32
|
ListRecordingsResponse,
|
|
33
|
+
ListTranscriptionsResponse,
|
|
33
34
|
MuteUsersRequest,
|
|
34
35
|
MuteUsersResponse,
|
|
35
36
|
OwnCapability,
|
|
@@ -48,10 +49,13 @@ import {
|
|
|
48
49
|
StartHLSBroadcastingResponse,
|
|
49
50
|
StartRecordingRequest,
|
|
50
51
|
StartRecordingResponse,
|
|
52
|
+
StartTranscriptionRequest,
|
|
53
|
+
StartTranscriptionResponse,
|
|
51
54
|
StatsOptions,
|
|
52
55
|
StopHLSBroadcastingResponse,
|
|
53
56
|
StopLiveResponse,
|
|
54
57
|
StopRecordingResponse,
|
|
58
|
+
StopTranscriptionResponse,
|
|
55
59
|
UnblockUserRequest,
|
|
56
60
|
UnblockUserResponse,
|
|
57
61
|
UnpinRequest,
|
|
@@ -509,10 +513,10 @@ export class Call {
|
|
|
509
513
|
|
|
510
514
|
this.clientStore.unregisterCall(this);
|
|
511
515
|
|
|
512
|
-
this.camera.
|
|
513
|
-
this.microphone.
|
|
514
|
-
this.screenShare.
|
|
515
|
-
this.speaker.
|
|
516
|
+
this.camera.dispose();
|
|
517
|
+
this.microphone.dispose();
|
|
518
|
+
this.screenShare.dispose();
|
|
519
|
+
this.speaker.dispose();
|
|
516
520
|
|
|
517
521
|
const stopOnLeavePromises: Promise<void>[] = [];
|
|
518
522
|
if (this.camera.stopOnLeave) {
|
|
@@ -1582,6 +1586,29 @@ export class Call {
|
|
|
1582
1586
|
);
|
|
1583
1587
|
};
|
|
1584
1588
|
|
|
1589
|
+
/**
|
|
1590
|
+
* Starts the transcription of the call.
|
|
1591
|
+
*
|
|
1592
|
+
* @param request the request data.
|
|
1593
|
+
*/
|
|
1594
|
+
startTranscription = async (
|
|
1595
|
+
request?: StartTranscriptionRequest,
|
|
1596
|
+
): Promise<StartTranscriptionResponse> => {
|
|
1597
|
+
return this.streamClient.post<
|
|
1598
|
+
StartTranscriptionResponse,
|
|
1599
|
+
StartTranscriptionRequest
|
|
1600
|
+
>(`${this.streamClientBasePath}/start_transcription`, request);
|
|
1601
|
+
};
|
|
1602
|
+
|
|
1603
|
+
/**
|
|
1604
|
+
* Stops the transcription of the call.
|
|
1605
|
+
*/
|
|
1606
|
+
stopTranscription = async (): Promise<StopTranscriptionResponse> => {
|
|
1607
|
+
return this.streamClient.post<StopTranscriptionResponse>(
|
|
1608
|
+
`${this.streamClientBasePath}/stop_transcription`,
|
|
1609
|
+
);
|
|
1610
|
+
};
|
|
1611
|
+
|
|
1585
1612
|
/**
|
|
1586
1613
|
* Sends a `call.permission_request` event to all users connected to the call. The call settings object contains infomration about which permissions can be requested during a call (for example a user might be allowed to request permission to publish audio, but not video).
|
|
1587
1614
|
*/
|
|
@@ -1859,6 +1886,17 @@ export class Call {
|
|
|
1859
1886
|
);
|
|
1860
1887
|
};
|
|
1861
1888
|
|
|
1889
|
+
/**
|
|
1890
|
+
* Retrieves the list of transcriptions for the current call.
|
|
1891
|
+
*
|
|
1892
|
+
* @returns the list of transcriptions.
|
|
1893
|
+
*/
|
|
1894
|
+
queryTranscriptions = async (): Promise<ListTranscriptionsResponse> => {
|
|
1895
|
+
return this.streamClient.get<ListTranscriptionsResponse>(
|
|
1896
|
+
`${this.streamClientBasePath}/transcriptions`,
|
|
1897
|
+
);
|
|
1898
|
+
};
|
|
1899
|
+
|
|
1862
1900
|
/**
|
|
1863
1901
|
* Retrieve call statistics for a particular call session (historical).
|
|
1864
1902
|
* Here `callSessionID` is mandatory.
|
package/src/StreamVideoClient.ts
CHANGED
|
@@ -13,6 +13,8 @@ import type {
|
|
|
13
13
|
ListDevicesResponse,
|
|
14
14
|
QueryCallsRequest,
|
|
15
15
|
QueryCallsResponse,
|
|
16
|
+
QueryCallStatsRequest,
|
|
17
|
+
QueryCallStatsResponse,
|
|
16
18
|
} from './gen/coordinator';
|
|
17
19
|
import {
|
|
18
20
|
AllClientEvents,
|
|
@@ -366,6 +368,19 @@ export class StreamVideoClient {
|
|
|
366
368
|
};
|
|
367
369
|
};
|
|
368
370
|
|
|
371
|
+
/**
|
|
372
|
+
* Retrieve the list of available call statistics reports matching a particular condition.
|
|
373
|
+
*
|
|
374
|
+
* @param data Filter and sort conditions for retrieving available call report summaries.
|
|
375
|
+
* @returns List with summary of available call reports matching the condition.
|
|
376
|
+
*/
|
|
377
|
+
queryCallStats = async (data: QueryCallStatsRequest = {}) => {
|
|
378
|
+
return this.streamClient.post<
|
|
379
|
+
QueryCallStatsResponse,
|
|
380
|
+
QueryCallStatsRequest
|
|
381
|
+
>(`/call/stats`, data);
|
|
382
|
+
};
|
|
383
|
+
|
|
369
384
|
/**
|
|
370
385
|
* Returns a list of available data centers available for hosting calls.
|
|
371
386
|
*/
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { combineLatest, Observable, pairwise } from 'rxjs';
|
|
2
2
|
import { Call } from '../Call';
|
|
3
3
|
import { CallingState } from '../store';
|
|
4
|
+
import { createSubscription } from '../store/rxUtils';
|
|
4
5
|
import { InputMediaDeviceManagerState } from './InputMediaDeviceManagerState';
|
|
5
6
|
import { isReactNative } from '../helpers/platforms';
|
|
6
7
|
import { Logger } from '../coordinator/connection/types';
|
|
@@ -8,6 +9,8 @@ import { getLogger } from '../logger';
|
|
|
8
9
|
import { TrackType } from '../gen/video/sfu/models/models';
|
|
9
10
|
import { deviceIds$ } from './devices';
|
|
10
11
|
|
|
12
|
+
export type MediaStreamFilter = (stream: MediaStream) => Promise<MediaStream>;
|
|
13
|
+
|
|
11
14
|
export abstract class InputMediaDeviceManager<
|
|
12
15
|
T extends InputMediaDeviceManagerState<C>,
|
|
13
16
|
C = MediaTrackConstraints,
|
|
@@ -25,8 +28,9 @@ export abstract class InputMediaDeviceManager<
|
|
|
25
28
|
*/
|
|
26
29
|
stopOnLeave = true;
|
|
27
30
|
logger: Logger;
|
|
28
|
-
private subscriptions:
|
|
31
|
+
private subscriptions: Function[] = [];
|
|
29
32
|
private isTrackStoppedDueToTrackEnd = false;
|
|
33
|
+
private filters: MediaStreamFilter[] = [];
|
|
30
34
|
|
|
31
35
|
protected constructor(
|
|
32
36
|
protected readonly call: Call,
|
|
@@ -113,6 +117,24 @@ export abstract class InputMediaDeviceManager<
|
|
|
113
117
|
}
|
|
114
118
|
}
|
|
115
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Registers a filter that will be applied to the stream.
|
|
122
|
+
*
|
|
123
|
+
* The registered filter will get the existing stream, and it should return
|
|
124
|
+
* a new stream with the applied filter.
|
|
125
|
+
*
|
|
126
|
+
* @param filter the filter to register.
|
|
127
|
+
* @returns a function that will unregister the filter.
|
|
128
|
+
*/
|
|
129
|
+
async registerFilter(filter: MediaStreamFilter) {
|
|
130
|
+
this.filters.push(filter);
|
|
131
|
+
await this.applySettingsToStream();
|
|
132
|
+
return async () => {
|
|
133
|
+
this.filters = this.filters.filter((f) => f !== filter);
|
|
134
|
+
await this.applySettingsToStream();
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
116
138
|
/**
|
|
117
139
|
* Will set the default constraints for the device.
|
|
118
140
|
*
|
|
@@ -141,8 +163,13 @@ export abstract class InputMediaDeviceManager<
|
|
|
141
163
|
await this.applySettingsToStream();
|
|
142
164
|
}
|
|
143
165
|
|
|
144
|
-
|
|
145
|
-
|
|
166
|
+
/**
|
|
167
|
+
* Disposes the manager.
|
|
168
|
+
*
|
|
169
|
+
* @internal
|
|
170
|
+
*/
|
|
171
|
+
dispose = () => {
|
|
172
|
+
this.subscriptions.forEach((s) => s());
|
|
146
173
|
};
|
|
147
174
|
|
|
148
175
|
protected async applySettingsToStream() {
|
|
@@ -229,7 +256,69 @@ export abstract class InputMediaDeviceManager<
|
|
|
229
256
|
...defaultConstraints,
|
|
230
257
|
deviceId: this.state.selectedDevice,
|
|
231
258
|
};
|
|
232
|
-
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Chains two media streams together.
|
|
262
|
+
*
|
|
263
|
+
* In our case, filters MediaStreams are derived from their parent MediaStream.
|
|
264
|
+
* However, once a child filter's track is stopped,
|
|
265
|
+
* the tracks of the parent MediaStream aren't automatically stopped.
|
|
266
|
+
* This leads to a situation where the camera indicator light is still on
|
|
267
|
+
* even though the user stopped publishing video.
|
|
268
|
+
*
|
|
269
|
+
* This function works around this issue by stopping the parent MediaStream's tracks
|
|
270
|
+
* as well once the child filter's tracks are stopped.
|
|
271
|
+
*
|
|
272
|
+
* It works by patching the stop() method of the child filter's tracks to also stop
|
|
273
|
+
* the parent MediaStream's tracks of the same type. Here we assume that
|
|
274
|
+
* the parent MediaStream has only one track of each type.
|
|
275
|
+
*
|
|
276
|
+
* @param parentStream the parent MediaStream. Omit for the root stream.
|
|
277
|
+
*/
|
|
278
|
+
const chainWith =
|
|
279
|
+
(parentStream?: Promise<MediaStream>) =>
|
|
280
|
+
async (filterStream: MediaStream): Promise<MediaStream> => {
|
|
281
|
+
if (!parentStream) return filterStream;
|
|
282
|
+
// TODO OL: take care of track.enabled property as well
|
|
283
|
+
const parent = await parentStream;
|
|
284
|
+
filterStream.getTracks().forEach((track) => {
|
|
285
|
+
const originalStop = track.stop;
|
|
286
|
+
track.stop = function stop() {
|
|
287
|
+
originalStop.call(track);
|
|
288
|
+
parent.getTracks().forEach((parentTrack) => {
|
|
289
|
+
if (parentTrack.kind === track.kind) {
|
|
290
|
+
parentTrack.stop();
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
};
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
parent.getTracks().forEach((parentTrack) => {
|
|
297
|
+
// When the parent stream abruptly ends, we propagate the event
|
|
298
|
+
// to the filter stream.
|
|
299
|
+
// This usually happens when the camera/microphone permissions
|
|
300
|
+
// are revoked or when the device is disconnected.
|
|
301
|
+
const handleParentTrackEnded = () => {
|
|
302
|
+
filterStream.getTracks().forEach((track) => {
|
|
303
|
+
if (parentTrack.kind !== track.kind) return;
|
|
304
|
+
track.stop();
|
|
305
|
+
track.dispatchEvent(new Event('ended')); // propagate the event
|
|
306
|
+
});
|
|
307
|
+
};
|
|
308
|
+
parentTrack.addEventListener('ended', handleParentTrackEnded);
|
|
309
|
+
this.subscriptions.push(() => {
|
|
310
|
+
parentTrack.removeEventListener('ended', handleParentTrackEnded);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
return filterStream;
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
// we publish the last MediaStream of the chain
|
|
318
|
+
stream = await this.filters.reduce(
|
|
319
|
+
(parent, filter) => parent.then(filter).then(chainWith(parent)),
|
|
320
|
+
this.getStream(constraints as C),
|
|
321
|
+
);
|
|
233
322
|
}
|
|
234
323
|
if (this.call.state.callingState === CallingState.JOINED) {
|
|
235
324
|
await this.publishStream(stream);
|
|
@@ -268,51 +357,54 @@ export abstract class InputMediaDeviceManager<
|
|
|
268
357
|
|
|
269
358
|
private handleDisconnectedOrReplacedDevices() {
|
|
270
359
|
this.subscriptions.push(
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if (isDeviceDisconnected) {
|
|
301
|
-
await this.disable();
|
|
302
|
-
this.select(undefined);
|
|
303
|
-
}
|
|
304
|
-
if (isDeviceReplaced) {
|
|
305
|
-
if (
|
|
306
|
-
this.isTrackStoppedDueToTrackEnd &&
|
|
307
|
-
this.state.status === 'disabled'
|
|
360
|
+
createSubscription(
|
|
361
|
+
combineLatest([
|
|
362
|
+
deviceIds$!.pipe(pairwise()),
|
|
363
|
+
this.state.selectedDevice$,
|
|
364
|
+
]),
|
|
365
|
+
async ([[prevDevices, currentDevices], deviceId]) => {
|
|
366
|
+
if (!deviceId) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (this.enablePromise) {
|
|
370
|
+
await this.enablePromise;
|
|
371
|
+
}
|
|
372
|
+
if (this.disablePromise) {
|
|
373
|
+
await this.disablePromise;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
let isDeviceDisconnected = false;
|
|
377
|
+
let isDeviceReplaced = false;
|
|
378
|
+
const currentDevice = this.findDeviceInList(currentDevices, deviceId);
|
|
379
|
+
const prevDevice = this.findDeviceInList(prevDevices, deviceId);
|
|
380
|
+
if (!currentDevice && prevDevice) {
|
|
381
|
+
isDeviceDisconnected = true;
|
|
382
|
+
} else if (
|
|
383
|
+
currentDevice &&
|
|
384
|
+
prevDevice &&
|
|
385
|
+
currentDevice.deviceId === prevDevice.deviceId &&
|
|
386
|
+
currentDevice.groupId !== prevDevice.groupId
|
|
308
387
|
) {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
388
|
+
isDeviceReplaced = true;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (isDeviceDisconnected) {
|
|
392
|
+
await this.disable();
|
|
393
|
+
this.select(undefined);
|
|
394
|
+
}
|
|
395
|
+
if (isDeviceReplaced) {
|
|
396
|
+
if (
|
|
397
|
+
this.isTrackStoppedDueToTrackEnd &&
|
|
398
|
+
this.state.status === 'disabled'
|
|
399
|
+
) {
|
|
400
|
+
await this.enable();
|
|
401
|
+
this.isTrackStoppedDueToTrackEnd = false;
|
|
402
|
+
} else {
|
|
403
|
+
await this.applySettingsToStream();
|
|
404
|
+
}
|
|
313
405
|
}
|
|
314
|
-
}
|
|
315
|
-
|
|
406
|
+
},
|
|
407
|
+
),
|
|
316
408
|
);
|
|
317
409
|
}
|
|
318
410
|
|
|
@@ -63,7 +63,12 @@ export class SpeakerManager {
|
|
|
63
63
|
this.state.setDevice(deviceId);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Disposes the manager.
|
|
68
|
+
*
|
|
69
|
+
* @internal
|
|
70
|
+
*/
|
|
71
|
+
dispose = () => {
|
|
67
72
|
this.subscriptions.forEach((s) => s.unsubscribe());
|
|
68
73
|
};
|
|
69
74
|
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { of } from 'rxjs';
|
|
3
|
+
import { Call } from '../../Call';
|
|
4
|
+
import { StreamClient } from '../../coordinator/connection/client';
|
|
5
|
+
import { StreamVideoWriteableStateStore } from '../../store';
|
|
6
|
+
import { InputMediaDeviceManagerState } from '../InputMediaDeviceManagerState';
|
|
7
|
+
import { InputMediaDeviceManager } from '../InputMediaDeviceManager';
|
|
8
|
+
import { mockVideoDevices, mockVideoStream } from './mocks';
|
|
9
|
+
import { TrackType } from '../../gen/video/sfu/models/models';
|
|
10
|
+
|
|
11
|
+
import '../../rtc/__tests__/mocks/webrtc.mocks';
|
|
12
|
+
|
|
13
|
+
class TestInputMediaDeviceManagerState extends InputMediaDeviceManagerState {
|
|
14
|
+
public getDeviceIdFromStream = vi.fn(
|
|
15
|
+
(stream) => stream.getTracks()[0].getSettings().deviceId,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
class TestInputMediaDeviceManager extends InputMediaDeviceManager<TestInputMediaDeviceManagerState> {
|
|
20
|
+
public getDevices = vi.fn(() => of(mockVideoDevices));
|
|
21
|
+
public getStream = vi.fn(() => Promise.resolve(mockVideoStream()));
|
|
22
|
+
public publishStream = vi.fn();
|
|
23
|
+
public stopPublishStream = vi.fn();
|
|
24
|
+
public getTracks = () => this.state.mediaStream?.getTracks() ?? [];
|
|
25
|
+
|
|
26
|
+
constructor(call: Call) {
|
|
27
|
+
super(
|
|
28
|
+
call,
|
|
29
|
+
new TestInputMediaDeviceManagerState(
|
|
30
|
+
'stop-tracks',
|
|
31
|
+
'camera' as PermissionName,
|
|
32
|
+
),
|
|
33
|
+
TrackType.VIDEO,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('MediaStream Filters', () => {
|
|
39
|
+
let manager: TestInputMediaDeviceManager;
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
manager = new TestInputMediaDeviceManager(
|
|
43
|
+
new Call({
|
|
44
|
+
id: '',
|
|
45
|
+
type: '',
|
|
46
|
+
streamClient: new StreamClient('abc123'),
|
|
47
|
+
clientStore: new StreamVideoWriteableStateStore(),
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should support registering and unregistering of filters', async () => {
|
|
53
|
+
const mediaStream = new MediaStream();
|
|
54
|
+
const track = new MediaStreamTrack();
|
|
55
|
+
vi.spyOn(mediaStream, 'getTracks').mockReturnValue([track]);
|
|
56
|
+
vi.spyOn(track, 'getSettings').mockReturnValue({ deviceId: '123' });
|
|
57
|
+
const filter = vi.fn().mockReturnValue(mediaStream);
|
|
58
|
+
const unregister = await manager.registerFilter(filter);
|
|
59
|
+
await manager.enable();
|
|
60
|
+
|
|
61
|
+
expect(filter).toHaveBeenCalled();
|
|
62
|
+
|
|
63
|
+
filter.mockClear();
|
|
64
|
+
await unregister();
|
|
65
|
+
await manager.enable();
|
|
66
|
+
|
|
67
|
+
expect(filter).not.toHaveBeenCalled();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should chain media streams together', async () => {
|
|
71
|
+
const createMediaStream = () => {
|
|
72
|
+
let onEndedCapture: EventListener | null = null;
|
|
73
|
+
const mediaStream = new MediaStream();
|
|
74
|
+
const track = new MediaStreamTrack();
|
|
75
|
+
vi.spyOn(mediaStream, 'getTracks').mockReturnValue([track]);
|
|
76
|
+
vi.spyOn(track, 'getSettings').mockReturnValue({ deviceId: '123' });
|
|
77
|
+
vi.spyOn(track, 'addEventListener').mockImplementation((_, fn) => {
|
|
78
|
+
onEndedCapture = fn as EventListener;
|
|
79
|
+
});
|
|
80
|
+
track.dispatchEvent = (event: Event) => {
|
|
81
|
+
if (onEndedCapture && event.type === 'ended') {
|
|
82
|
+
onEndedCapture(event);
|
|
83
|
+
}
|
|
84
|
+
return true;
|
|
85
|
+
};
|
|
86
|
+
return mediaStream;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const rootMediaStream = createMediaStream();
|
|
90
|
+
const filterMediaStream = createMediaStream();
|
|
91
|
+
const filter1 = vi.fn().mockReturnValue(rootMediaStream);
|
|
92
|
+
const filter2 = vi.fn().mockReturnValue(filterMediaStream);
|
|
93
|
+
await manager.registerFilter(filter1);
|
|
94
|
+
await manager.registerFilter(filter2);
|
|
95
|
+
await manager.enable();
|
|
96
|
+
|
|
97
|
+
expect(filter1).toHaveBeenCalled();
|
|
98
|
+
expect(filter2).toHaveBeenCalled();
|
|
99
|
+
|
|
100
|
+
// stopping should bubble up: filter -> root
|
|
101
|
+
const rootSpy = vi.spyOn(rootMediaStream.getTracks()[0], 'stop');
|
|
102
|
+
expect(rootSpy).not.toHaveBeenCalled();
|
|
103
|
+
filterMediaStream.getTracks()[0].stop();
|
|
104
|
+
expect(rootSpy).toHaveBeenCalled();
|
|
105
|
+
|
|
106
|
+
// stopping should not bubble down: root -> filter
|
|
107
|
+
const filterSpy = vi.spyOn(filterMediaStream.getTracks()[0], 'stop');
|
|
108
|
+
rootMediaStream.getTracks()[0].stop();
|
|
109
|
+
expect(filterSpy).not.toHaveBeenCalled();
|
|
110
|
+
|
|
111
|
+
// simulate an abrupt ending of the track
|
|
112
|
+
rootMediaStream.getTracks()[0].dispatchEvent(new Event('ended'));
|
|
113
|
+
expect(filterSpy).toHaveBeenCalled();
|
|
114
|
+
});
|
|
115
|
+
});
|