@stream-io/video-client 1.42.1 → 1.42.2
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 +6 -0
- package/dist/index.browser.es.js +116 -48
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +116 -48
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +116 -48
- package/dist/index.es.js.map +1 -1
- package/dist/src/StreamSfuClient.d.ts +1 -6
- package/dist/src/errors/SfuJoinError.d.ts +7 -0
- package/dist/src/errors/index.d.ts +1 -0
- package/dist/src/gen/coordinator/index.d.ts +6 -0
- package/dist/src/rtc/BasePeerConnection.d.ts +1 -0
- package/dist/src/rtc/Dispatcher.d.ts +36 -2
- package/index.ts +1 -0
- package/package.json +1 -1
- package/src/Call.ts +22 -5
- package/src/StreamSfuClient.ts +36 -37
- package/src/errors/SfuJoinError.ts +26 -0
- package/src/errors/index.ts +1 -0
- package/src/events/internal.ts +4 -4
- package/src/events/speaker.ts +2 -2
- package/src/gen/coordinator/index.ts +6 -0
- package/src/rtc/BasePeerConnection.ts +3 -1
- package/src/rtc/Dispatcher.ts +55 -13
- package/src/rtc/__tests__/Dispatcher.test.ts +28 -0
- package/src/rtc/__tests__/Publisher.test.ts +3 -0
- package/src/rtc/__tests__/Subscriber.test.ts +2 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Dispatcher, IceTrickleBuffer } from './rtc';
|
|
2
|
-
import {
|
|
2
|
+
import { JoinRequest, JoinResponse } from './gen/video/sfu/event/events';
|
|
3
3
|
import { ICERestartRequest, SendAnswerRequest, SendStatsRequest, SetPublisherRequest, TrackMuteState, TrackSubscriptionDetails } from './gen/video/sfu/signal_rpc/signal';
|
|
4
4
|
import { ICETrickle } from './gen/video/sfu/models/models';
|
|
5
5
|
import { StreamClient } from './coordinator/connection/client';
|
|
@@ -170,8 +170,3 @@ export declare class StreamSfuClient {
|
|
|
170
170
|
private keepAlive;
|
|
171
171
|
private scheduleConnectionCheck;
|
|
172
172
|
}
|
|
173
|
-
export declare class SfuJoinError extends Error {
|
|
174
|
-
errorEvent: SfuErrorEvent;
|
|
175
|
-
unrecoverable: boolean;
|
|
176
|
-
constructor(event: SfuErrorEvent);
|
|
177
|
-
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Error as SfuErrorEvent } from '../gen/video/sfu/event/events';
|
|
2
|
+
export declare class SfuJoinError extends Error {
|
|
3
|
+
errorEvent: SfuErrorEvent;
|
|
4
|
+
unrecoverable: boolean;
|
|
5
|
+
constructor(event: SfuErrorEvent);
|
|
6
|
+
static isJoinErrorCode(event: SfuErrorEvent): boolean;
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './SfuJoinError';
|
|
@@ -5358,6 +5358,12 @@ export interface JoinCallRequest {
|
|
|
5358
5358
|
* @memberof JoinCallRequest
|
|
5359
5359
|
*/
|
|
5360
5360
|
migrating_from?: string;
|
|
5361
|
+
/**
|
|
5362
|
+
* List of SFU IDs to exclude when picking a new SFU for the participant
|
|
5363
|
+
* @type {Array<string>}
|
|
5364
|
+
* @memberof JoinCallRequest
|
|
5365
|
+
*/
|
|
5366
|
+
migrating_from_list?: Array<string>;
|
|
5361
5367
|
/**
|
|
5362
5368
|
*
|
|
5363
5369
|
* @type {boolean}
|
|
@@ -18,6 +18,7 @@ export declare abstract class BasePeerConnection {
|
|
|
18
18
|
protected readonly state: CallState;
|
|
19
19
|
protected readonly dispatcher: Dispatcher;
|
|
20
20
|
protected readonly clientPublishOptions?: ClientPublishOptions;
|
|
21
|
+
protected readonly tag: string;
|
|
21
22
|
protected sfuClient: StreamSfuClient;
|
|
22
23
|
private onReconnectionNeeded?;
|
|
23
24
|
private readonly iceRestartDelay;
|
|
@@ -15,11 +15,45 @@ export type DispatchableMessage<K extends SfuEventKinds> = {
|
|
|
15
15
|
[Key in K]: AllSfuEvents[Key];
|
|
16
16
|
};
|
|
17
17
|
};
|
|
18
|
+
/**
|
|
19
|
+
* Determines if a given event name belongs to the category of SFU events.
|
|
20
|
+
*
|
|
21
|
+
* @param eventName the name of the event to check.
|
|
22
|
+
* @returns true if the event name is an SFU event, otherwise false.
|
|
23
|
+
*/
|
|
18
24
|
export declare const isSfuEvent: (eventName: SfuEventKinds | EventTypes) => eventName is SfuEventKinds;
|
|
19
25
|
export declare class Dispatcher {
|
|
20
26
|
private readonly logger;
|
|
21
27
|
private subscribers;
|
|
28
|
+
/**
|
|
29
|
+
* Dispatch an event to all subscribers.
|
|
30
|
+
*
|
|
31
|
+
* @param message the event payload to dispatch.
|
|
32
|
+
* @param tag for scoping events to a specific tag. Use `*` dispatch to every tag.
|
|
33
|
+
*/
|
|
22
34
|
dispatch: <K extends SfuEventKinds>(message: DispatchableMessage<K>, tag?: string) => void;
|
|
23
|
-
|
|
24
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Emit an event to a list of listeners.
|
|
37
|
+
*
|
|
38
|
+
* @param payload the event payload to emit.
|
|
39
|
+
* @param listeners the list of listeners to emit the event to.
|
|
40
|
+
*/
|
|
41
|
+
emit: (payload: any, listeners?: CallEventListener<any>[]) => void;
|
|
42
|
+
/**
|
|
43
|
+
* Subscribe to an event.
|
|
44
|
+
*
|
|
45
|
+
* @param eventName the name of the event to subscribe to.
|
|
46
|
+
* @param tag for scoping events to a specific tag. Use `*` dispatch to every tag.
|
|
47
|
+
* @param fn the callback function to invoke when the event is emitted.
|
|
48
|
+
* @returns a function that can be called to unsubscribe from the event.
|
|
49
|
+
*/
|
|
50
|
+
on: <E extends keyof AllSfuEvents>(eventName: E, tag: string, fn: CallEventListener<E>) => () => void;
|
|
51
|
+
/**
|
|
52
|
+
* Unsubscribe from an event.
|
|
53
|
+
*
|
|
54
|
+
* @param eventName the name of the event to unsubscribe from.
|
|
55
|
+
* @param tag for scoping events to a specific tag. Use `*` dispatch to every tag.
|
|
56
|
+
* @param fn the callback function to remove from the event listeners.
|
|
57
|
+
*/
|
|
58
|
+
off: <E extends keyof AllSfuEvents>(eventName: E, tag: string, fn: CallEventListener<E>) => void;
|
|
25
59
|
}
|
package/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ export * from './src/CallType';
|
|
|
14
14
|
export * from './src/StreamVideoClient';
|
|
15
15
|
export * from './src/StreamSfuClient';
|
|
16
16
|
export * from './src/devices';
|
|
17
|
+
export * from './src/errors';
|
|
17
18
|
export * from './src/store';
|
|
18
19
|
export * from './src/sorting';
|
|
19
20
|
export * from './src/helpers/client-details';
|
package/package.json
CHANGED
package/src/Call.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { StreamSfuClient } from './StreamSfuClient';
|
|
2
|
+
import { SfuJoinError } from './errors';
|
|
2
3
|
import {
|
|
3
4
|
BasePeerConnectionOpts,
|
|
4
5
|
Dispatcher,
|
|
@@ -561,7 +562,7 @@ export class Call {
|
|
|
561
562
|
fn: CallEventListener<E>,
|
|
562
563
|
) => {
|
|
563
564
|
if (isSfuEvent(eventName)) {
|
|
564
|
-
return this.dispatcher.on(eventName, fn);
|
|
565
|
+
return this.dispatcher.on(eventName, '*', fn);
|
|
565
566
|
}
|
|
566
567
|
|
|
567
568
|
const offHandler = this.streamClient.on(eventName, (e) => {
|
|
@@ -589,7 +590,7 @@ export class Call {
|
|
|
589
590
|
fn: CallEventListener<E>,
|
|
590
591
|
) => {
|
|
591
592
|
if (isSfuEvent(eventName)) {
|
|
592
|
-
return this.dispatcher.off(eventName, fn);
|
|
593
|
+
return this.dispatcher.off(eventName, '*', fn);
|
|
593
594
|
}
|
|
594
595
|
|
|
595
596
|
// unsubscribe from the stream client event by using the 'off' reference
|
|
@@ -918,6 +919,7 @@ export class Call {
|
|
|
918
919
|
this.logger.trace(`Joining call (${attempt})`, this.cid);
|
|
919
920
|
await this.doJoin(data);
|
|
920
921
|
delete joinData.migrating_from;
|
|
922
|
+
delete joinData.migrating_from_list;
|
|
921
923
|
break;
|
|
922
924
|
} catch (err) {
|
|
923
925
|
this.logger.warn(`Failed to join call (${attempt})`, this.cid);
|
|
@@ -931,11 +933,17 @@ export class Call {
|
|
|
931
933
|
throw err;
|
|
932
934
|
}
|
|
933
935
|
|
|
936
|
+
// immediately switch to a different SFU in case of recoverable join error
|
|
937
|
+
const switchSfu =
|
|
938
|
+
err instanceof SfuJoinError &&
|
|
939
|
+
SfuJoinError.isJoinErrorCode(err.errorEvent);
|
|
940
|
+
|
|
934
941
|
const sfuId = this.credentials?.server.edge_name || '';
|
|
935
942
|
const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
|
|
936
943
|
sfuJoinFailures.set(sfuId, failures);
|
|
937
|
-
if (failures >= 2) {
|
|
944
|
+
if (switchSfu || failures >= 2) {
|
|
938
945
|
joinData.migrating_from = sfuId;
|
|
946
|
+
joinData.migrating_from_list = Array.from(sfuJoinFailures.keys());
|
|
939
947
|
}
|
|
940
948
|
|
|
941
949
|
if (attempt === maxJoinRetries - 1) {
|
|
@@ -1611,11 +1619,16 @@ export class Call {
|
|
|
1611
1619
|
|
|
1612
1620
|
try {
|
|
1613
1621
|
const currentSfu = currentSfuClient.edgeName;
|
|
1614
|
-
await this.doJoin({
|
|
1622
|
+
await this.doJoin({
|
|
1623
|
+
...this.joinCallData,
|
|
1624
|
+
migrating_from: currentSfu,
|
|
1625
|
+
migrating_from_list: [currentSfu],
|
|
1626
|
+
});
|
|
1615
1627
|
} finally {
|
|
1616
1628
|
// cleanup the migration_from field after the migration is complete or failed
|
|
1617
1629
|
// as we don't want to keep dirty data in the join call data
|
|
1618
1630
|
delete this.joinCallData?.migrating_from;
|
|
1631
|
+
delete this.joinCallData?.migrating_from_list;
|
|
1619
1632
|
}
|
|
1620
1633
|
|
|
1621
1634
|
await this.restorePublishedTracks();
|
|
@@ -1660,6 +1673,10 @@ export class Call {
|
|
|
1660
1673
|
// handles the "error" event, through which the SFU can request a reconnect
|
|
1661
1674
|
const unregisterOnError = this.on('error', (e) => {
|
|
1662
1675
|
const { reconnectStrategy: strategy, error } = e;
|
|
1676
|
+
// SFU_FULL is a join error, and when emitted, although it specifies a
|
|
1677
|
+
// `migrate` strategy, we should actually perform a REJOIN to a new SFU.
|
|
1678
|
+
// This is now handled separately in the `call.join()` method.
|
|
1679
|
+
if (SfuJoinError.isJoinErrorCode(e)) return;
|
|
1663
1680
|
if (strategy === WebsocketReconnectStrategy.UNSPECIFIED) return;
|
|
1664
1681
|
if (strategy === WebsocketReconnectStrategy.DISCONNECT) {
|
|
1665
1682
|
this.leave({ message: 'SFU instructed to disconnect' }).catch((err) => {
|
package/src/StreamSfuClient.ts
CHANGED
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
SfuEventKinds,
|
|
15
15
|
} from './rtc';
|
|
16
16
|
import {
|
|
17
|
-
Error as SfuErrorEvent,
|
|
18
17
|
JoinRequest,
|
|
19
18
|
JoinResponse,
|
|
20
19
|
SfuRequest,
|
|
@@ -27,10 +26,7 @@ import {
|
|
|
27
26
|
TrackMuteState,
|
|
28
27
|
TrackSubscriptionDetails,
|
|
29
28
|
} from './gen/video/sfu/signal_rpc/signal';
|
|
30
|
-
import {
|
|
31
|
-
ICETrickle,
|
|
32
|
-
WebsocketReconnectStrategy,
|
|
33
|
-
} from './gen/video/sfu/models/models';
|
|
29
|
+
import { ICETrickle } from './gen/video/sfu/models/models';
|
|
34
30
|
import { StreamClient } from './coordinator/connection/client';
|
|
35
31
|
import { generateUUIDv4 } from './coordinator/connection/utils';
|
|
36
32
|
import { Credentials } from './gen/coordinator';
|
|
@@ -43,6 +39,7 @@ import {
|
|
|
43
39
|
} from './helpers/promise';
|
|
44
40
|
import { getTimers } from './timers';
|
|
45
41
|
import { Tracer, TraceSlice } from './stats';
|
|
42
|
+
import { SfuJoinError } from './errors';
|
|
46
43
|
|
|
47
44
|
export type StreamSfuClientConstructor = {
|
|
48
45
|
/**
|
|
@@ -250,8 +247,8 @@ export class StreamSfuClient {
|
|
|
250
247
|
// In that case, those events (ICE candidates) need to be buffered
|
|
251
248
|
// and later added to the appropriate PeerConnection
|
|
252
249
|
// once the remoteDescription is known and set.
|
|
253
|
-
this.unsubscribeIceTrickle = dispatcher.on('iceTrickle', (
|
|
254
|
-
this.iceTrickleBuffer.push(
|
|
250
|
+
this.unsubscribeIceTrickle = dispatcher.on('iceTrickle', tag, (t) => {
|
|
251
|
+
this.iceTrickleBuffer.push(t);
|
|
255
252
|
});
|
|
256
253
|
|
|
257
254
|
// listen to network changes to handle offline state
|
|
@@ -504,6 +501,7 @@ export class StreamSfuClient {
|
|
|
504
501
|
const task = (this.migrationTask = promiseWithResolvers());
|
|
505
502
|
const unsubscribe = this.dispatcher.on(
|
|
506
503
|
'participantMigrationComplete',
|
|
504
|
+
this.tag,
|
|
507
505
|
() => {
|
|
508
506
|
unsubscribe();
|
|
509
507
|
clearTimeout(this.migrateAwayTimeout);
|
|
@@ -541,27 +539,40 @@ export class StreamSfuClient {
|
|
|
541
539
|
const current = this.joinResponseTask;
|
|
542
540
|
|
|
543
541
|
let timeoutId: NodeJS.Timeout | undefined = undefined;
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
clearTimeout(timeoutId);
|
|
549
|
-
unsubscribe?.();
|
|
550
|
-
unsubscribeJoinErrorEvents();
|
|
551
|
-
current.reject(new SfuJoinError(event));
|
|
552
|
-
}
|
|
553
|
-
});
|
|
554
|
-
const unsubscribe = this.dispatcher.on('joinResponse', (joinResponse) => {
|
|
542
|
+
let unsubscribeJoinResponse: (() => void) | undefined = undefined;
|
|
543
|
+
let unsubscribeJoinErrorEvents: (() => void) | undefined = undefined;
|
|
544
|
+
|
|
545
|
+
const cleanupJoinSubscriptions = () => {
|
|
555
546
|
clearTimeout(timeoutId);
|
|
556
|
-
|
|
557
|
-
unsubscribeJoinErrorEvents();
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
547
|
+
timeoutId = undefined;
|
|
548
|
+
unsubscribeJoinErrorEvents?.();
|
|
549
|
+
unsubscribeJoinErrorEvents = undefined;
|
|
550
|
+
unsubscribeJoinResponse?.();
|
|
551
|
+
unsubscribeJoinResponse = undefined;
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
unsubscribeJoinErrorEvents = this.dispatcher.on(
|
|
555
|
+
'error',
|
|
556
|
+
this.tag,
|
|
557
|
+
(event) => {
|
|
558
|
+
if (SfuJoinError.isJoinErrorCode(event)) {
|
|
559
|
+
cleanupJoinSubscriptions();
|
|
560
|
+
current.reject(new SfuJoinError(event));
|
|
561
|
+
}
|
|
562
|
+
},
|
|
563
|
+
);
|
|
564
|
+
unsubscribeJoinResponse = this.dispatcher.on(
|
|
565
|
+
'joinResponse',
|
|
566
|
+
this.tag,
|
|
567
|
+
(joinResponse) => {
|
|
568
|
+
cleanupJoinSubscriptions();
|
|
569
|
+
this.keepAlive();
|
|
570
|
+
current.resolve(joinResponse);
|
|
571
|
+
},
|
|
572
|
+
);
|
|
561
573
|
|
|
562
574
|
timeoutId = setTimeout(() => {
|
|
563
|
-
|
|
564
|
-
unsubscribeJoinErrorEvents();
|
|
575
|
+
cleanupJoinSubscriptions();
|
|
565
576
|
const message = `Waiting for "joinResponse" has timed out after ${this.joinResponseTimeout}ms`;
|
|
566
577
|
this.tracer?.trace('joinRequestTimeout', message);
|
|
567
578
|
current.reject(new Error(message));
|
|
@@ -647,15 +658,3 @@ export class StreamSfuClient {
|
|
|
647
658
|
}, this.unhealthyTimeoutInMs);
|
|
648
659
|
};
|
|
649
660
|
}
|
|
650
|
-
|
|
651
|
-
export class SfuJoinError extends Error {
|
|
652
|
-
errorEvent: SfuErrorEvent;
|
|
653
|
-
unrecoverable: boolean;
|
|
654
|
-
|
|
655
|
-
constructor(event: SfuErrorEvent) {
|
|
656
|
-
super(event.error?.message || 'Join Error');
|
|
657
|
-
this.errorEvent = event;
|
|
658
|
-
this.unrecoverable =
|
|
659
|
-
event.reconnectStrategy === WebsocketReconnectStrategy.DISCONNECT;
|
|
660
|
-
}
|
|
661
|
-
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Error as SfuErrorEvent } from '../gen/video/sfu/event/events';
|
|
2
|
+
import {
|
|
3
|
+
ErrorCode,
|
|
4
|
+
WebsocketReconnectStrategy,
|
|
5
|
+
} from '../gen/video/sfu/models/models';
|
|
6
|
+
|
|
7
|
+
export class SfuJoinError extends Error {
|
|
8
|
+
errorEvent: SfuErrorEvent;
|
|
9
|
+
unrecoverable: boolean;
|
|
10
|
+
|
|
11
|
+
constructor(event: SfuErrorEvent) {
|
|
12
|
+
super(event.error?.message || 'Join Error');
|
|
13
|
+
this.errorEvent = event;
|
|
14
|
+
this.unrecoverable =
|
|
15
|
+
event.reconnectStrategy === WebsocketReconnectStrategy.DISCONNECT;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static isJoinErrorCode(event: SfuErrorEvent): boolean {
|
|
19
|
+
const code = event.error?.code;
|
|
20
|
+
return (
|
|
21
|
+
code === ErrorCode.SFU_FULL ||
|
|
22
|
+
code === ErrorCode.SFU_SHUTTING_DOWN ||
|
|
23
|
+
code === ErrorCode.CALL_PARTICIPANT_LIMIT_REACHED
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './SfuJoinError';
|
package/src/events/internal.ts
CHANGED
|
@@ -18,7 +18,7 @@ export const watchConnectionQualityChanged = (
|
|
|
18
18
|
dispatcher: Dispatcher,
|
|
19
19
|
state: CallState,
|
|
20
20
|
) => {
|
|
21
|
-
return dispatcher.on('connectionQualityChanged', (e) => {
|
|
21
|
+
return dispatcher.on('connectionQualityChanged', '*', (e) => {
|
|
22
22
|
const { connectionQualityUpdates } = e;
|
|
23
23
|
if (!connectionQualityUpdates) return;
|
|
24
24
|
state.updateParticipants(
|
|
@@ -44,7 +44,7 @@ export const watchParticipantCountChanged = (
|
|
|
44
44
|
dispatcher: Dispatcher,
|
|
45
45
|
state: CallState,
|
|
46
46
|
) => {
|
|
47
|
-
return dispatcher.on('healthCheckResponse', (e) => {
|
|
47
|
+
return dispatcher.on('healthCheckResponse', '*', (e) => {
|
|
48
48
|
const { participantCount } = e;
|
|
49
49
|
if (participantCount) {
|
|
50
50
|
state.setParticipantCount(participantCount.total);
|
|
@@ -54,7 +54,7 @@ export const watchParticipantCountChanged = (
|
|
|
54
54
|
};
|
|
55
55
|
|
|
56
56
|
export const watchLiveEnded = (dispatcher: Dispatcher, call: Call) => {
|
|
57
|
-
return dispatcher.on('error', (e) => {
|
|
57
|
+
return dispatcher.on('error', '*', (e) => {
|
|
58
58
|
if (e.error && e.error.code !== ErrorCode.LIVE_ENDED) return;
|
|
59
59
|
|
|
60
60
|
call.state.setBackstage(true);
|
|
@@ -70,7 +70,7 @@ export const watchLiveEnded = (dispatcher: Dispatcher, call: Call) => {
|
|
|
70
70
|
* Watches and logs the errors reported by the currently connected SFU.
|
|
71
71
|
*/
|
|
72
72
|
export const watchSfuErrorReports = (dispatcher: Dispatcher) => {
|
|
73
|
-
return dispatcher.on('error', (e) => {
|
|
73
|
+
return dispatcher.on('error', '*', (e) => {
|
|
74
74
|
if (!e.error) return;
|
|
75
75
|
const logger = videoLoggerSystem.getLogger('SfuClient');
|
|
76
76
|
const { error, reconnectStrategy } = e;
|
package/src/events/speaker.ts
CHANGED
|
@@ -9,7 +9,7 @@ export const watchDominantSpeakerChanged = (
|
|
|
9
9
|
dispatcher: Dispatcher,
|
|
10
10
|
state: CallState,
|
|
11
11
|
) => {
|
|
12
|
-
return dispatcher.on('dominantSpeakerChanged', (e) => {
|
|
12
|
+
return dispatcher.on('dominantSpeakerChanged', '*', (e) => {
|
|
13
13
|
const { sessionId } = e;
|
|
14
14
|
if (sessionId === state.dominantSpeaker?.sessionId) return;
|
|
15
15
|
state.setParticipants((participants) =>
|
|
@@ -41,7 +41,7 @@ export const watchAudioLevelChanged = (
|
|
|
41
41
|
dispatcher: Dispatcher,
|
|
42
42
|
state: CallState,
|
|
43
43
|
) => {
|
|
44
|
-
return dispatcher.on('audioLevelChanged', (e) => {
|
|
44
|
+
return dispatcher.on('audioLevelChanged', '*', (e) => {
|
|
45
45
|
const { audioLevels } = e;
|
|
46
46
|
state.updateParticipants(
|
|
47
47
|
audioLevels.reduce<StreamVideoParticipantPatches>((patches, current) => {
|
|
@@ -5362,6 +5362,12 @@ export interface JoinCallRequest {
|
|
|
5362
5362
|
* @memberof JoinCallRequest
|
|
5363
5363
|
*/
|
|
5364
5364
|
migrating_from?: string;
|
|
5365
|
+
/**
|
|
5366
|
+
* List of SFU IDs to exclude when picking a new SFU for the participant
|
|
5367
|
+
* @type {Array<string>}
|
|
5368
|
+
* @memberof JoinCallRequest
|
|
5369
|
+
*/
|
|
5370
|
+
migrating_from_list?: Array<string>;
|
|
5365
5371
|
/**
|
|
5366
5372
|
*
|
|
5367
5373
|
* @type {boolean}
|
|
@@ -27,6 +27,7 @@ export abstract class BasePeerConnection {
|
|
|
27
27
|
protected readonly state: CallState;
|
|
28
28
|
protected readonly dispatcher: Dispatcher;
|
|
29
29
|
protected readonly clientPublishOptions?: ClientPublishOptions;
|
|
30
|
+
protected readonly tag: string;
|
|
30
31
|
protected sfuClient: StreamSfuClient;
|
|
31
32
|
|
|
32
33
|
private onReconnectionNeeded?: OnReconnectionNeeded;
|
|
@@ -67,6 +68,7 @@ export abstract class BasePeerConnection {
|
|
|
67
68
|
this.dispatcher = dispatcher;
|
|
68
69
|
this.iceRestartDelay = iceRestartDelay;
|
|
69
70
|
this.clientPublishOptions = clientPublishOptions;
|
|
71
|
+
this.tag = tag;
|
|
70
72
|
this.onReconnectionNeeded = onReconnectionNeeded;
|
|
71
73
|
this.logger = videoLoggerSystem.getLogger(
|
|
72
74
|
peerType === PeerType.SUBSCRIBER ? 'Subscriber' : 'Publisher',
|
|
@@ -162,7 +164,7 @@ export abstract class BasePeerConnection {
|
|
|
162
164
|
fn: CallEventListener<E>,
|
|
163
165
|
): void => {
|
|
164
166
|
this.subscriptions.push(
|
|
165
|
-
this.dispatcher.on(event, (e) => {
|
|
167
|
+
this.dispatcher.on(event, this.tag, (e) => {
|
|
166
168
|
const lockKey = `pc.${this.lock}.${event}`;
|
|
167
169
|
withoutConcurrency(lockKey, async () => fn(e)).catch((err) => {
|
|
168
170
|
if (this.isDisposed) return;
|
package/src/rtc/Dispatcher.ts
CHANGED
|
@@ -46,53 +46,95 @@ const sfuEventKinds: Record<SfuEventKinds, undefined> = {
|
|
|
46
46
|
inboundStateNotification: undefined,
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Determines if a given event name belongs to the category of SFU events.
|
|
51
|
+
*
|
|
52
|
+
* @param eventName the name of the event to check.
|
|
53
|
+
* @returns true if the event name is an SFU event, otherwise false.
|
|
54
|
+
*/
|
|
49
55
|
export const isSfuEvent = (
|
|
50
56
|
eventName: SfuEventKinds | EventTypes,
|
|
51
57
|
): eventName is SfuEventKinds => {
|
|
52
58
|
return Object.prototype.hasOwnProperty.call(sfuEventKinds, eventName);
|
|
53
59
|
};
|
|
54
60
|
|
|
61
|
+
type TaggedHandler = { [tag: string]: CallEventListener<any>[] | undefined };
|
|
62
|
+
|
|
55
63
|
export class Dispatcher {
|
|
56
64
|
private readonly logger = videoLoggerSystem.getLogger('Dispatcher');
|
|
57
|
-
private subscribers: Partial<
|
|
58
|
-
Record<SfuEventKinds, CallEventListener<any>[] | undefined>
|
|
59
|
-
> = {};
|
|
65
|
+
private subscribers: Partial<Record<SfuEventKinds, TaggedHandler>> = {};
|
|
60
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Dispatch an event to all subscribers.
|
|
69
|
+
*
|
|
70
|
+
* @param message the event payload to dispatch.
|
|
71
|
+
* @param tag for scoping events to a specific tag. Use `*` dispatch to every tag.
|
|
72
|
+
*/
|
|
61
73
|
dispatch = <K extends SfuEventKinds>(
|
|
62
74
|
message: DispatchableMessage<K>,
|
|
63
|
-
tag: string = '
|
|
75
|
+
tag: string = '*',
|
|
64
76
|
) => {
|
|
65
77
|
const eventKind = message.eventPayload.oneofKind;
|
|
66
78
|
if (!eventKind) return;
|
|
67
79
|
const payload = message.eventPayload[eventKind];
|
|
68
80
|
this.logger.debug(`Dispatching ${eventKind}, tag=${tag}`, payload);
|
|
69
|
-
const
|
|
70
|
-
if (!
|
|
71
|
-
|
|
81
|
+
const handlers = this.subscribers[eventKind];
|
|
82
|
+
if (!handlers) return;
|
|
83
|
+
this.emit(payload, handlers[tag]);
|
|
84
|
+
if (tag !== '*') this.emit(payload, handlers['*']);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Emit an event to a list of listeners.
|
|
89
|
+
*
|
|
90
|
+
* @param payload the event payload to emit.
|
|
91
|
+
* @param listeners the list of listeners to emit the event to.
|
|
92
|
+
*/
|
|
93
|
+
emit = (payload: any, listeners: CallEventListener<any>[] = []) => {
|
|
94
|
+
for (const listener of listeners) {
|
|
72
95
|
try {
|
|
73
|
-
|
|
96
|
+
listener(payload);
|
|
74
97
|
} catch (e) {
|
|
75
98
|
this.logger.warn('Listener failed with error', e);
|
|
76
99
|
}
|
|
77
100
|
}
|
|
78
101
|
};
|
|
79
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Subscribe to an event.
|
|
105
|
+
*
|
|
106
|
+
* @param eventName the name of the event to subscribe to.
|
|
107
|
+
* @param tag for scoping events to a specific tag. Use `*` dispatch to every tag.
|
|
108
|
+
* @param fn the callback function to invoke when the event is emitted.
|
|
109
|
+
* @returns a function that can be called to unsubscribe from the event.
|
|
110
|
+
*/
|
|
80
111
|
on = <E extends keyof AllSfuEvents>(
|
|
81
112
|
eventName: E,
|
|
113
|
+
tag: string,
|
|
82
114
|
fn: CallEventListener<E>,
|
|
83
115
|
) => {
|
|
84
|
-
(this.subscribers[eventName] ??=
|
|
116
|
+
const bucket = (this.subscribers[eventName] ??= {} as TaggedHandler);
|
|
117
|
+
(bucket[tag] ??= []).push(fn);
|
|
85
118
|
return () => {
|
|
86
|
-
this.off(eventName, fn);
|
|
119
|
+
this.off(eventName, tag, fn);
|
|
87
120
|
};
|
|
88
121
|
};
|
|
89
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Unsubscribe from an event.
|
|
125
|
+
*
|
|
126
|
+
* @param eventName the name of the event to unsubscribe from.
|
|
127
|
+
* @param tag for scoping events to a specific tag. Use `*` dispatch to every tag.
|
|
128
|
+
* @param fn the callback function to remove from the event listeners.
|
|
129
|
+
*/
|
|
90
130
|
off = <E extends keyof AllSfuEvents>(
|
|
91
131
|
eventName: E,
|
|
132
|
+
tag: string,
|
|
92
133
|
fn: CallEventListener<E>,
|
|
93
134
|
) => {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
);
|
|
135
|
+
const bucket = this.subscribers[eventName];
|
|
136
|
+
const listeners = bucket?.[tag];
|
|
137
|
+
if (!listeners) return;
|
|
138
|
+
bucket[tag] = listeners.filter((f) => f !== fn);
|
|
97
139
|
};
|
|
98
140
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { DispatchableMessage, Dispatcher } from '../Dispatcher';
|
|
3
|
+
|
|
4
|
+
describe('Dispatcher', () => {
|
|
5
|
+
it('routes events to tag listeners and wildcard listeners', () => {
|
|
6
|
+
const dispatcher = new Dispatcher();
|
|
7
|
+
const taggedListener = vi.fn();
|
|
8
|
+
const wildcardListener = vi.fn();
|
|
9
|
+
|
|
10
|
+
dispatcher.on('healthCheckResponse', 'tag-1', taggedListener);
|
|
11
|
+
dispatcher.on('healthCheckResponse', '*', wildcardListener);
|
|
12
|
+
|
|
13
|
+
const message: DispatchableMessage<'healthCheckResponse'> = {
|
|
14
|
+
eventPayload: {
|
|
15
|
+
oneofKind: 'healthCheckResponse',
|
|
16
|
+
healthCheckResponse: {} as never,
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
dispatcher.dispatch(message, 'tag-1');
|
|
21
|
+
expect(taggedListener).toHaveBeenCalledTimes(1);
|
|
22
|
+
expect(wildcardListener).toHaveBeenCalledTimes(1);
|
|
23
|
+
|
|
24
|
+
dispatcher.dispatch(message, 'tag-2');
|
|
25
|
+
expect(taggedListener).toHaveBeenCalledTimes(1);
|
|
26
|
+
expect(wildcardListener).toHaveBeenCalledTimes(2);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -180,6 +180,7 @@ describe('Publisher', () => {
|
|
|
180
180
|
},
|
|
181
181
|
},
|
|
182
182
|
}) as DispatchableMessage<'changePublishQuality'>,
|
|
183
|
+
'test',
|
|
183
184
|
);
|
|
184
185
|
expect(publisher['changePublishQuality']).toHaveBeenCalled();
|
|
185
186
|
});
|
|
@@ -193,6 +194,7 @@ describe('Publisher', () => {
|
|
|
193
194
|
changePublishOptions: { publishOptions: [], reason: 'test' },
|
|
194
195
|
},
|
|
195
196
|
}) as DispatchableMessage<'changePublishOptions'>,
|
|
197
|
+
'test',
|
|
196
198
|
);
|
|
197
199
|
expect(publisher['syncPublishOptions']).toHaveBeenCalled();
|
|
198
200
|
});
|
|
@@ -210,6 +212,7 @@ describe('Publisher', () => {
|
|
|
210
212
|
},
|
|
211
213
|
},
|
|
212
214
|
}) as DispatchableMessage<'iceRestart'>,
|
|
215
|
+
'test',
|
|
213
216
|
);
|
|
214
217
|
expect(publisher.restartIce).toHaveBeenCalled();
|
|
215
218
|
});
|
|
@@ -36,7 +36,7 @@ describe('Subscriber', () => {
|
|
|
36
36
|
sessionId: 'sessionId',
|
|
37
37
|
streamClient: new StreamClient('abc'),
|
|
38
38
|
cid: 'test:123',
|
|
39
|
-
tag: '
|
|
39
|
+
tag: 'test',
|
|
40
40
|
credentials: {
|
|
41
41
|
server: {
|
|
42
42
|
url: 'https://getstream.io/',
|
|
@@ -255,6 +255,7 @@ describe('Subscriber', () => {
|
|
|
255
255
|
subscriberOffer,
|
|
256
256
|
},
|
|
257
257
|
}) as DispatchableMessage<'subscriberOffer'>,
|
|
258
|
+
'test',
|
|
258
259
|
);
|
|
259
260
|
|
|
260
261
|
// @ts-expect-error - private method
|