@stream-io/video-client 0.0.13 → 0.0.15
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 +237 -130
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +239 -129
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +237 -130
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +2 -1
- package/dist/src/StreamSfuClient.d.ts +1 -0
- package/dist/src/StreamVideoClient.d.ts +5 -1
- package/dist/src/coordinator/connection/types.d.ts +3 -2
- package/dist/src/coordinator/connection/utils.d.ts +2 -1
- package/dist/src/logger.d.ts +4 -0
- package/dist/src/rtc/Dispatcher.d.ts +2 -0
- package/dist/src/rtc/IceTrickleBuffer.d.ts +2 -0
- package/dist/src/rtc/publisher.d.ts +1 -0
- package/dist/src/store/CallState.d.ts +2 -0
- package/dist/src/types.d.ts +1 -1
- package/index.ts +1 -0
- package/package.json +1 -1
- package/src/Call.ts +69 -42
- package/src/StreamSfuClient.ts +70 -29
- package/src/StreamVideoClient.ts +46 -3
- package/src/coordinator/connection/client.ts +22 -29
- package/src/coordinator/connection/connection.ts +2 -3
- package/src/coordinator/connection/connection_fallback.ts +0 -1
- package/src/coordinator/connection/types.ts +4 -2
- package/src/coordinator/connection/utils.ts +5 -2
- package/src/devices/devices.ts +10 -3
- package/src/events/__tests__/call-permissions.test.ts +2 -2
- package/src/events/call.ts +11 -4
- package/src/events/sessions.ts +7 -2
- package/src/logger.ts +45 -0
- package/src/rtc/Dispatcher.ts +14 -4
- package/src/rtc/IceTrickleBuffer.ts +8 -1
- package/src/rtc/__tests__/publisher.test.ts +1 -1
- package/src/rtc/codecs.ts +7 -5
- package/src/rtc/flows/join.ts +4 -1
- package/src/rtc/publisher.ts +31 -12
- package/src/rtc/signal.ts +8 -7
- package/src/rtc/subscriber.ts +16 -10
- package/src/stats/state-store-stats-reporter.ts +12 -4
- package/src/store/CallState.ts +7 -2
- package/src/types.ts +3 -2
package/dist/src/Call.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { AcceptCallResponse, BlockUserResponse, EndCallResponse, GetCallResponse
|
|
|
5
5
|
import { CallConstructor, CallLeaveOptions, DebounceType, JoinCallData, PublishOptions, SubscriptionChanges } from './types';
|
|
6
6
|
import { ViewportTracker } from './helpers/ViewportTracker';
|
|
7
7
|
import { PermissionsContext } from './permissions';
|
|
8
|
-
import { CallEventHandler, CallEventTypes, StreamCallEvent } from './coordinator/connection/types';
|
|
8
|
+
import { CallEventHandler, CallEventTypes, Logger, StreamCallEvent } from './coordinator/connection/types';
|
|
9
9
|
/**
|
|
10
10
|
* An object representation of a `Call`.
|
|
11
11
|
*/
|
|
@@ -49,6 +49,7 @@ export declare class Call {
|
|
|
49
49
|
* The permissions context of this call.
|
|
50
50
|
*/
|
|
51
51
|
readonly permissionsContext: PermissionsContext;
|
|
52
|
+
readonly logger: Logger;
|
|
52
53
|
/**
|
|
53
54
|
* The event dispatcher instance dedicated to this Call instance.
|
|
54
55
|
* @private
|
|
@@ -2,7 +2,7 @@ import { Call } from './Call';
|
|
|
2
2
|
import { StreamClient } from './coordinator/connection/client';
|
|
3
3
|
import { StreamVideoReadOnlyStateStore } from './store';
|
|
4
4
|
import type { ConnectedEvent, CreateCallTypeRequest, CreateCallTypeResponse, CreateDeviceRequest, CreateGuestRequest, CreateGuestResponse, GetCallTypeResponse, GetEdgesResponse, ListCallTypeResponse, ListDevicesResponse, QueryCallsRequest, UpdateCallTypeRequest, UpdateCallTypeResponse } from './gen/coordinator';
|
|
5
|
-
import type { EventHandler, EventTypes, StreamClientOptions, TokenOrProvider, TokenProvider, User } from './coordinator/connection/types';
|
|
5
|
+
import type { EventHandler, EventTypes, LogLevel, Logger, StreamClientOptions, TokenOrProvider, TokenProvider, User } from './coordinator/connection/types';
|
|
6
6
|
/**
|
|
7
7
|
* A `StreamVideoClient` instance lets you communicate with our API, and authenticate users.
|
|
8
8
|
*/
|
|
@@ -13,11 +13,14 @@ export declare class StreamVideoClient {
|
|
|
13
13
|
readonly readOnlyStateStore: StreamVideoReadOnlyStateStore;
|
|
14
14
|
readonly user?: User;
|
|
15
15
|
readonly token?: TokenOrProvider;
|
|
16
|
+
readonly logLevel: LogLevel;
|
|
17
|
+
readonly logger: Logger;
|
|
16
18
|
private readonly writeableStateStore;
|
|
17
19
|
streamClient: StreamClient;
|
|
18
20
|
private eventHandlersToUnregister;
|
|
19
21
|
private connectionPromise;
|
|
20
22
|
private disconnectionPromise;
|
|
23
|
+
private logLevels;
|
|
21
24
|
/**
|
|
22
25
|
* You should create only one instance of `StreamVideoClient`.
|
|
23
26
|
*/
|
|
@@ -144,4 +147,5 @@ export declare class StreamVideoClient {
|
|
|
144
147
|
* @param tokenOrProvider a token or a function that returns a token.
|
|
145
148
|
*/
|
|
146
149
|
private connectAnonymousUser;
|
|
150
|
+
private filterLogs;
|
|
147
151
|
}
|
|
@@ -20,7 +20,7 @@ export type UserWithId = (UserRequest & {
|
|
|
20
20
|
});
|
|
21
21
|
export type { OwnUserResponse } from '../../gen/coordinator';
|
|
22
22
|
export type ConnectAPIResponse = Promise<void | ConnectedEvent>;
|
|
23
|
-
export type LogLevel = 'info' | 'error' | 'warn';
|
|
23
|
+
export type LogLevel = 'debug' | 'info' | 'error' | 'warn';
|
|
24
24
|
type ErrorResponseDetails = {
|
|
25
25
|
code: number;
|
|
26
26
|
messages: string[];
|
|
@@ -59,7 +59,7 @@ export type EventHandler = (event: StreamVideoEvent) => void;
|
|
|
59
59
|
export type CallEventHandler = (event: StreamCallEvent) => void;
|
|
60
60
|
export type EventTypes = 'all' | StreamVideoEvent['type'];
|
|
61
61
|
export type CallEventTypes = StreamCallEvent['type'];
|
|
62
|
-
export type Logger = (logLevel: LogLevel, message: string, extraData?:
|
|
62
|
+
export type Logger = (logLevel: LogLevel, message: string, extraData?: any, tags?: string[]) => void;
|
|
63
63
|
export type StreamClientOptions = Partial<AxiosRequestConfig> & {
|
|
64
64
|
/**
|
|
65
65
|
* Used to disable warnings that are triggered by using connectUser or connectAnonymousUser server-side.
|
|
@@ -76,6 +76,7 @@ export type StreamClientOptions = Partial<AxiosRequestConfig> & {
|
|
|
76
76
|
/** experimental feature, please contact support if you want this feature enabled for you */
|
|
77
77
|
enableWSFallback?: boolean;
|
|
78
78
|
logger?: Logger;
|
|
79
|
+
logLevel?: LogLevel;
|
|
79
80
|
/**
|
|
80
81
|
* When true, user will be persisted on client. Otherwise if `connectUser` call fails, then you need to
|
|
81
82
|
* call `connectUser` again to retry.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Logger } from './types';
|
|
1
2
|
export declare const sleep: (m: number) => Promise<void>;
|
|
2
3
|
export declare function isFunction<T>(value: Function | T): value is Function;
|
|
3
4
|
/**
|
|
@@ -21,7 +22,7 @@ export declare function convertErrorToJson(err: Error): Record<string, unknown>;
|
|
|
21
22
|
* isOnline safely return the navigator.online value for browser env
|
|
22
23
|
* if navigator is not in global object, it always return true
|
|
23
24
|
*/
|
|
24
|
-
export declare function isOnline(): boolean;
|
|
25
|
+
export declare function isOnline(logger: Logger): boolean;
|
|
25
26
|
/**
|
|
26
27
|
* listenForConnectionChanges - Adds an event listener fired on browser going online or offline
|
|
27
28
|
*/
|
|
@@ -5,6 +5,8 @@ export declare const isSfuEvent: (eventName: SfuEventKinds | CallEventTypes) =>
|
|
|
5
5
|
export type SfuEventListener = (event: SfuEvent) => void;
|
|
6
6
|
export declare class Dispatcher {
|
|
7
7
|
private subscribers;
|
|
8
|
+
private logger?;
|
|
9
|
+
constructor();
|
|
8
10
|
dispatch: (message: SfuEvent) => void;
|
|
9
11
|
on: (eventName: SfuEventKinds, fn: SfuEventListener) => () => void;
|
|
10
12
|
off: (eventName: SfuEventKinds, fn: SfuEventListener) => void;
|
|
@@ -7,5 +7,7 @@ import { ICETrickle } from '../gen/video/sfu/models/models';
|
|
|
7
7
|
export declare class IceTrickleBuffer {
|
|
8
8
|
readonly subscriberCandidates: ReplaySubject<ICETrickle>;
|
|
9
9
|
readonly publisherCandidates: ReplaySubject<ICETrickle>;
|
|
10
|
+
private logger?;
|
|
11
|
+
constructor();
|
|
10
12
|
push: (iceTrickle: ICETrickle) => void;
|
|
11
13
|
}
|
|
@@ -23,6 +23,7 @@ export declare class Publisher {
|
|
|
23
23
|
private isDtxEnabled;
|
|
24
24
|
private isRedEnabled;
|
|
25
25
|
private preferredVideoCodec?;
|
|
26
|
+
private logger?;
|
|
26
27
|
constructor({ connectionConfig, sfuClient, state, isDtxEnabled, isRedEnabled, preferredVideoCodec, }: PublisherOpts);
|
|
27
28
|
/**
|
|
28
29
|
* Starts publishing the given track of the given media stream.
|
|
@@ -4,6 +4,7 @@ import { StreamVideoLocalParticipant, StreamVideoParticipant, StreamVideoPartici
|
|
|
4
4
|
import { CallStatsReport } from '../stats/types';
|
|
5
5
|
import { CallRecording, CallResponse, MemberResponse, OwnCapability, PermissionRequestEvent } from '../gen/coordinator';
|
|
6
6
|
import { Comparator } from '../sorting';
|
|
7
|
+
import { Logger } from '../coordinator/connection/types';
|
|
7
8
|
/**
|
|
8
9
|
* Represents the state of the current call.
|
|
9
10
|
*/
|
|
@@ -201,6 +202,7 @@ export declare class CallState {
|
|
|
201
202
|
* The calling state.
|
|
202
203
|
*/
|
|
203
204
|
callingState$: Observable<CallingState>;
|
|
205
|
+
readonly logger: Logger;
|
|
204
206
|
/**
|
|
205
207
|
* A list of comparators that are used to sort the participants.
|
|
206
208
|
*
|
package/dist/src/types.d.ts
CHANGED
package/index.ts
CHANGED
package/package.json
CHANGED
package/src/Call.ts
CHANGED
|
@@ -100,11 +100,13 @@ import {
|
|
|
100
100
|
CallEventHandler,
|
|
101
101
|
CallEventTypes,
|
|
102
102
|
EventHandler,
|
|
103
|
+
Logger,
|
|
103
104
|
StreamCallEvent,
|
|
104
105
|
} from './coordinator/connection/types';
|
|
105
106
|
import { UAParser } from 'ua-parser-js';
|
|
106
107
|
import { getSdkInfo } from './sdk-info';
|
|
107
108
|
import { isReactNative } from './helpers/platforms';
|
|
109
|
+
import { getLogger } from './logger';
|
|
108
110
|
|
|
109
111
|
/**
|
|
110
112
|
* An object representation of a `Call`.
|
|
@@ -160,6 +162,7 @@ export class Call {
|
|
|
160
162
|
* The permissions context of this call.
|
|
161
163
|
*/
|
|
162
164
|
readonly permissionsContext = new PermissionsContext();
|
|
165
|
+
readonly logger: Logger;
|
|
163
166
|
|
|
164
167
|
/**
|
|
165
168
|
* The event dispatcher instance dedicated to this Call instance.
|
|
@@ -220,6 +223,7 @@ export class Call {
|
|
|
220
223
|
this.streamClient = streamClient;
|
|
221
224
|
this.clientStore = clientStore;
|
|
222
225
|
this.streamClientBasePath = `/call/${this.type}/${this.id}`;
|
|
226
|
+
this.logger = getLogger(['call']);
|
|
223
227
|
|
|
224
228
|
const callTypeConfig = CallTypes.get(type);
|
|
225
229
|
const participantSorter =
|
|
@@ -280,7 +284,7 @@ export class Call {
|
|
|
280
284
|
);
|
|
281
285
|
if (!hasPermission && this.publisher.isPublishing(trackType)) {
|
|
282
286
|
this.stopPublish(trackType).catch((err) => {
|
|
283
|
-
|
|
287
|
+
this.logger('error', `Error stopping publish ${trackType}`, err);
|
|
284
288
|
});
|
|
285
289
|
}
|
|
286
290
|
}
|
|
@@ -579,6 +583,12 @@ export class Call {
|
|
|
579
583
|
throw new Error(`Illegal State: Already joined.`);
|
|
580
584
|
}
|
|
581
585
|
|
|
586
|
+
if (this.state.callingState === CallingState.LEFT) {
|
|
587
|
+
throw new Error(
|
|
588
|
+
'Illegal State: Cannot join already left call. Create a new Call instance to join a call.',
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
|
|
582
592
|
const previousCallingState = this.state.callingState;
|
|
583
593
|
this.state.setCallingState(CallingState.JOINING);
|
|
584
594
|
|
|
@@ -641,7 +651,10 @@ export class Call {
|
|
|
641
651
|
* A closure which hides away the re-connection logic.
|
|
642
652
|
*/
|
|
643
653
|
const rejoin = async () => {
|
|
644
|
-
|
|
654
|
+
this.logger(
|
|
655
|
+
'debug',
|
|
656
|
+
`Rejoining call ${this.cid} (${this.reconnectAttempts})...`,
|
|
657
|
+
);
|
|
645
658
|
this.reconnectAttempts++;
|
|
646
659
|
this.state.setCallingState(CallingState.RECONNECTING);
|
|
647
660
|
|
|
@@ -656,7 +669,7 @@ export class Call {
|
|
|
656
669
|
|
|
657
670
|
await sleep(retryInterval(this.reconnectAttempts));
|
|
658
671
|
await this.join(data);
|
|
659
|
-
|
|
672
|
+
this.logger('info', `Rejoin: ${this.reconnectAttempts} successful!`);
|
|
660
673
|
if (localParticipant && !isReactNative()) {
|
|
661
674
|
const {
|
|
662
675
|
audioStream,
|
|
@@ -669,7 +682,7 @@ export class Call {
|
|
|
669
682
|
if (videoStream) await this.publishVideoStream(videoStream);
|
|
670
683
|
if (screenShare) await this.publishScreenShareStream(screenShare);
|
|
671
684
|
}
|
|
672
|
-
|
|
685
|
+
this.logger('info', `Rejoin: state restored ${this.reconnectAttempts}`);
|
|
673
686
|
};
|
|
674
687
|
|
|
675
688
|
this.rejoinPromise = rejoin;
|
|
@@ -688,13 +701,14 @@ export class Call {
|
|
|
688
701
|
if (isReactNative()) return;
|
|
689
702
|
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
690
703
|
rejoin().catch(() => {
|
|
691
|
-
|
|
704
|
+
this.logger(
|
|
705
|
+
'error',
|
|
692
706
|
`Rejoin failed for ${this.reconnectAttempts} times. Giving up.`,
|
|
693
707
|
);
|
|
694
708
|
this.state.setCallingState(CallingState.RECONNECTING_FAILED);
|
|
695
709
|
});
|
|
696
710
|
} else {
|
|
697
|
-
|
|
711
|
+
this.logger('error', 'Reconnect attempts exceeded. Giving up...');
|
|
698
712
|
this.state.setCallingState(CallingState.RECONNECTING_FAILED);
|
|
699
713
|
}
|
|
700
714
|
});
|
|
@@ -705,16 +719,17 @@ export class Call {
|
|
|
705
719
|
if (typeof window !== 'undefined' && window.addEventListener) {
|
|
706
720
|
const handleOnOffline = () => {
|
|
707
721
|
window.removeEventListener('offline', handleOnOffline);
|
|
708
|
-
|
|
722
|
+
this.logger('warn', 'Join: Going offline...');
|
|
709
723
|
this.state.setCallingState(CallingState.OFFLINE);
|
|
710
724
|
};
|
|
711
725
|
|
|
712
726
|
const handleOnOnline = () => {
|
|
713
727
|
window.removeEventListener('online', handleOnOnline);
|
|
714
728
|
if (this.state.callingState === CallingState.OFFLINE) {
|
|
715
|
-
|
|
729
|
+
this.logger('info', 'Join: Going online...');
|
|
716
730
|
rejoin().catch(() => {
|
|
717
|
-
|
|
731
|
+
this.logger(
|
|
732
|
+
'error',
|
|
718
733
|
`Rejoin failed for ${this.reconnectAttempts} times. Giving up.`,
|
|
719
734
|
);
|
|
720
735
|
this.state.setCallingState(CallingState.RECONNECTING_FAILED);
|
|
@@ -783,7 +798,7 @@ export class Call {
|
|
|
783
798
|
clientDetails.sdk = getSdkInfo();
|
|
784
799
|
// 1. wait for the signal server to be ready before sending "joinRequest"
|
|
785
800
|
sfuClient.signalReady
|
|
786
|
-
.catch((err) =>
|
|
801
|
+
.catch((err) => this.logger('error', 'Signal ready failed', err))
|
|
787
802
|
// prepare a generic SDP and send it to the SFU.
|
|
788
803
|
// this is a throw-away SDP that the SFU will use to determine
|
|
789
804
|
// the capabilities of the client (codec support, etc.)
|
|
@@ -794,12 +809,15 @@ export class Call {
|
|
|
794
809
|
this.streamClient.options.preferredVideoCodec,
|
|
795
810
|
),
|
|
796
811
|
)
|
|
797
|
-
.then((sdp) =>
|
|
798
|
-
|
|
812
|
+
.then((sdp) => {
|
|
813
|
+
const joinRequest = {
|
|
799
814
|
subscriberSdp: sdp || '',
|
|
800
815
|
clientDetails,
|
|
801
|
-
}
|
|
802
|
-
|
|
816
|
+
};
|
|
817
|
+
this.logger('info', 'Sending join request to SFU');
|
|
818
|
+
this.logger('debug', 'Join request payload', joinRequest);
|
|
819
|
+
sfuClient.join(joinRequest);
|
|
820
|
+
});
|
|
803
821
|
|
|
804
822
|
// 2. in parallel, wait for the SFU to send us the "joinResponse"
|
|
805
823
|
// this will throw an error if the SFU rejects the join request or
|
|
@@ -813,7 +831,7 @@ export class Call {
|
|
|
813
831
|
this.state.setParticipants(
|
|
814
832
|
currentParticipants.map<StreamVideoParticipant>((participant) => ({
|
|
815
833
|
...participant,
|
|
816
|
-
|
|
834
|
+
isLocalParticipant: participant.sessionId === sfuClient.sessionId,
|
|
817
835
|
viewportVisibilityState: VisibilityState.UNKNOWN,
|
|
818
836
|
})),
|
|
819
837
|
);
|
|
@@ -823,14 +841,15 @@ export class Call {
|
|
|
823
841
|
|
|
824
842
|
this.reconnectAttempts = 0; // reset the reconnect attempts counter
|
|
825
843
|
this.state.setCallingState(CallingState.JOINED);
|
|
826
|
-
|
|
844
|
+
this.logger('info', `Joined call ${this.cid}`);
|
|
827
845
|
} catch (err) {
|
|
828
846
|
// join failed, try to rejoin
|
|
829
847
|
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
830
848
|
await rejoin();
|
|
831
|
-
|
|
849
|
+
this.logger('info', `Rejoin ${this.reconnectAttempts} successful!`);
|
|
832
850
|
} else {
|
|
833
|
-
|
|
851
|
+
this.logger(
|
|
852
|
+
'error',
|
|
834
853
|
`Rejoin failed for ${this.reconnectAttempts} times. Giving up.`,
|
|
835
854
|
);
|
|
836
855
|
this.state.setCallingState(CallingState.RECONNECTING_FAILED);
|
|
@@ -862,7 +881,8 @@ export class Call {
|
|
|
862
881
|
|
|
863
882
|
const [videoTrack] = videoStream.getVideoTracks();
|
|
864
883
|
if (!videoTrack) {
|
|
865
|
-
|
|
884
|
+
this.logger('error', `There is no video track to publish in the stream.`);
|
|
885
|
+
return;
|
|
866
886
|
}
|
|
867
887
|
|
|
868
888
|
await this.publisher.publishStream(
|
|
@@ -893,7 +913,8 @@ export class Call {
|
|
|
893
913
|
|
|
894
914
|
const [audioTrack] = audioStream.getAudioTracks();
|
|
895
915
|
if (!audioTrack) {
|
|
896
|
-
|
|
916
|
+
this.logger('error', `There is no audio track in the stream to publish`);
|
|
917
|
+
return;
|
|
897
918
|
}
|
|
898
919
|
|
|
899
920
|
await this.publisher.publishStream(
|
|
@@ -922,7 +943,11 @@ export class Call {
|
|
|
922
943
|
|
|
923
944
|
const [screenShareTrack] = screenShareStream.getVideoTracks();
|
|
924
945
|
if (!screenShareTrack) {
|
|
925
|
-
|
|
946
|
+
this.logger(
|
|
947
|
+
'error',
|
|
948
|
+
`There is no video track in the screen share stream to publish`,
|
|
949
|
+
);
|
|
950
|
+
return;
|
|
926
951
|
}
|
|
927
952
|
|
|
928
953
|
await this.publisher.publishStream(
|
|
@@ -942,7 +967,7 @@ export class Call {
|
|
|
942
967
|
* @param trackType the track type to stop publishing.
|
|
943
968
|
*/
|
|
944
969
|
stopPublish = async (trackType: TrackType) => {
|
|
945
|
-
|
|
970
|
+
this.logger('info', `stopPublish ${TrackType[trackType]}`);
|
|
946
971
|
await this.publisher?.unpublishStream(trackType);
|
|
947
972
|
};
|
|
948
973
|
|
|
@@ -992,7 +1017,7 @@ export class Call {
|
|
|
992
1017
|
const subscriptions: TrackSubscriptionDetails[] = [];
|
|
993
1018
|
participants.forEach((p) => {
|
|
994
1019
|
// we don't want to subscribe to our own tracks
|
|
995
|
-
if (p.
|
|
1020
|
+
if (p.isLocalParticipant) return;
|
|
996
1021
|
|
|
997
1022
|
// NOTE: audio tracks don't have to be requested explicitly
|
|
998
1023
|
// as the SFU will implicitly subscribe us to all of them,
|
|
@@ -1116,39 +1141,38 @@ export class Call {
|
|
|
1116
1141
|
const [primaryStream] = e.streams;
|
|
1117
1142
|
// example: `e3f6aaf8-b03d-4911-be36-83f47d37a76a:TRACK_TYPE_VIDEO`
|
|
1118
1143
|
const [trackId, trackType] = primaryStream.id.split(':');
|
|
1119
|
-
|
|
1144
|
+
this.logger('info', `Got remote ${trackType} track:`);
|
|
1145
|
+
this.logger('debug', `Track: `, e.track);
|
|
1120
1146
|
const participantToUpdate = this.state.participants.find(
|
|
1121
1147
|
(p) => p.trackLookupPrefix === trackId,
|
|
1122
1148
|
);
|
|
1123
1149
|
if (!participantToUpdate) {
|
|
1124
|
-
|
|
1150
|
+
this.logger(
|
|
1151
|
+
'error',
|
|
1152
|
+
`'Received track for unknown participant: ${trackId}'`,
|
|
1153
|
+
e,
|
|
1154
|
+
);
|
|
1125
1155
|
return;
|
|
1126
1156
|
}
|
|
1127
1157
|
|
|
1128
1158
|
e.track.addEventListener('mute', () => {
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
participantToUpdate.userId
|
|
1132
|
-
`${trackType}:${trackId}`,
|
|
1133
|
-
e.track,
|
|
1159
|
+
this.logger(
|
|
1160
|
+
'info',
|
|
1161
|
+
`Track muted: ${participantToUpdate.userId} ${trackType}:${trackId}`,
|
|
1134
1162
|
);
|
|
1135
1163
|
});
|
|
1136
1164
|
|
|
1137
1165
|
e.track.addEventListener('unmute', () => {
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
participantToUpdate.userId
|
|
1141
|
-
`${trackType}:${trackId}`,
|
|
1142
|
-
e.track,
|
|
1166
|
+
this.logger(
|
|
1167
|
+
'info',
|
|
1168
|
+
`Track unmuted: ${participantToUpdate.userId} ${trackType}:${trackId}`,
|
|
1143
1169
|
);
|
|
1144
1170
|
});
|
|
1145
1171
|
|
|
1146
1172
|
e.track.addEventListener('ended', () => {
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
participantToUpdate.userId
|
|
1150
|
-
`${trackType}:${trackId}`,
|
|
1151
|
-
e.track,
|
|
1173
|
+
this.logger(
|
|
1174
|
+
'info',
|
|
1175
|
+
`Track ended: ${participantToUpdate.userId} ${trackType}:${trackId}`,
|
|
1152
1176
|
);
|
|
1153
1177
|
});
|
|
1154
1178
|
|
|
@@ -1161,12 +1185,15 @@ export class Call {
|
|
|
1161
1185
|
)[trackType];
|
|
1162
1186
|
|
|
1163
1187
|
if (!streamKindProp) {
|
|
1164
|
-
|
|
1188
|
+
this.logger('error', `Unknown track type: ${trackType}`);
|
|
1165
1189
|
return;
|
|
1166
1190
|
}
|
|
1167
1191
|
const previousStream = participantToUpdate[streamKindProp];
|
|
1168
1192
|
if (previousStream) {
|
|
1169
|
-
|
|
1193
|
+
this.logger(
|
|
1194
|
+
'info',
|
|
1195
|
+
`Cleaning up previous remote tracks: ${e.track.kind}`,
|
|
1196
|
+
);
|
|
1170
1197
|
previousStream.getTracks().forEach((t) => {
|
|
1171
1198
|
t.stop();
|
|
1172
1199
|
previousStream.removeTrack(t);
|
package/src/StreamSfuClient.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
FinishedUnaryCall,
|
|
3
|
+
MethodInfo,
|
|
4
|
+
NextUnaryFn,
|
|
5
|
+
RpcInterceptor,
|
|
6
|
+
RpcOptions,
|
|
7
|
+
UnaryCall,
|
|
8
|
+
} from '@protobuf-ts/runtime-rpc';
|
|
2
9
|
import { SignalServerClient } from './gen/video/sfu/signal_rpc/signal.client';
|
|
3
10
|
import { createSignalClient, withHeaders } from './rpc';
|
|
4
11
|
import {
|
|
@@ -23,6 +30,8 @@ import {
|
|
|
23
30
|
retryInterval,
|
|
24
31
|
sleep,
|
|
25
32
|
} from './coordinator/connection/utils';
|
|
33
|
+
import { Logger } from './coordinator/connection/types';
|
|
34
|
+
import { getLogger } from './logger';
|
|
26
35
|
|
|
27
36
|
export type StreamSfuClientConstructor = {
|
|
28
37
|
/**
|
|
@@ -84,6 +93,7 @@ export class StreamSfuClient {
|
|
|
84
93
|
private unhealthyTimeoutInMs = this.pingIntervalInMs + 5 * 1000;
|
|
85
94
|
private lastMessageTimestamp?: Date;
|
|
86
95
|
private readonly unsubscribeIceTrickle: () => void;
|
|
96
|
+
private readonly logger: Logger;
|
|
87
97
|
|
|
88
98
|
/**
|
|
89
99
|
* Constructs a new SFU client.
|
|
@@ -103,12 +113,27 @@ export class StreamSfuClient {
|
|
|
103
113
|
}: StreamSfuClientConstructor) {
|
|
104
114
|
this.sessionId = sessionId || generateUUIDv4();
|
|
105
115
|
this.token = token;
|
|
116
|
+
this.logger = getLogger(['sfu-client']);
|
|
117
|
+
const logger = this.logger;
|
|
118
|
+
const logInterceptor: RpcInterceptor = {
|
|
119
|
+
interceptUnary(
|
|
120
|
+
next: NextUnaryFn,
|
|
121
|
+
method: MethodInfo,
|
|
122
|
+
input: object,
|
|
123
|
+
options: RpcOptions,
|
|
124
|
+
): UnaryCall {
|
|
125
|
+
logger('info', `Calling SFU RPC method ${method.name}`);
|
|
126
|
+
logger('debug', `Method call payload`, { input, options });
|
|
127
|
+
return next(method, input, options);
|
|
128
|
+
},
|
|
129
|
+
};
|
|
106
130
|
this.rpc = createSignalClient({
|
|
107
131
|
baseUrl: url,
|
|
108
132
|
interceptors: [
|
|
109
133
|
withHeaders({
|
|
110
134
|
Authorization: `Bearer ${token}`,
|
|
111
135
|
}),
|
|
136
|
+
logInterceptor,
|
|
112
137
|
],
|
|
113
138
|
});
|
|
114
139
|
|
|
@@ -152,38 +177,46 @@ export class StreamSfuClient {
|
|
|
152
177
|
};
|
|
153
178
|
|
|
154
179
|
updateSubscriptions = async (subscriptions: TrackSubscriptionDetails[]) => {
|
|
155
|
-
return retryable(
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
180
|
+
return retryable(
|
|
181
|
+
() =>
|
|
182
|
+
this.rpc.updateSubscriptions({
|
|
183
|
+
sessionId: this.sessionId,
|
|
184
|
+
tracks: subscriptions,
|
|
185
|
+
}),
|
|
186
|
+
this.logger,
|
|
160
187
|
);
|
|
161
188
|
};
|
|
162
189
|
|
|
163
190
|
setPublisher = async (data: Omit<SetPublisherRequest, 'sessionId'>) => {
|
|
164
|
-
return retryable(
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
191
|
+
return retryable(
|
|
192
|
+
() =>
|
|
193
|
+
this.rpc.setPublisher({
|
|
194
|
+
...data,
|
|
195
|
+
sessionId: this.sessionId,
|
|
196
|
+
}),
|
|
197
|
+
this.logger,
|
|
169
198
|
);
|
|
170
199
|
};
|
|
171
200
|
|
|
172
201
|
sendAnswer = async (data: Omit<SendAnswerRequest, 'sessionId'>) => {
|
|
173
|
-
return retryable(
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
202
|
+
return retryable(
|
|
203
|
+
() =>
|
|
204
|
+
this.rpc.sendAnswer({
|
|
205
|
+
...data,
|
|
206
|
+
sessionId: this.sessionId,
|
|
207
|
+
}),
|
|
208
|
+
this.logger,
|
|
178
209
|
);
|
|
179
210
|
};
|
|
180
211
|
|
|
181
212
|
iceTrickle = async (data: Omit<ICETrickle, 'sessionId'>) => {
|
|
182
|
-
return retryable(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
213
|
+
return retryable(
|
|
214
|
+
() =>
|
|
215
|
+
this.rpc.iceTrickle({
|
|
216
|
+
...data,
|
|
217
|
+
sessionId: this.sessionId,
|
|
218
|
+
}),
|
|
219
|
+
this.logger,
|
|
187
220
|
);
|
|
188
221
|
};
|
|
189
222
|
|
|
@@ -201,11 +234,13 @@ export class StreamSfuClient {
|
|
|
201
234
|
updateMuteStates = async (
|
|
202
235
|
data: Omit<UpdateMuteStatesRequest, 'sessionId'>,
|
|
203
236
|
) => {
|
|
204
|
-
return retryable(
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
237
|
+
return retryable(
|
|
238
|
+
() =>
|
|
239
|
+
this.rpc.updateMuteStates({
|
|
240
|
+
...data,
|
|
241
|
+
sessionId: this.sessionId,
|
|
242
|
+
}),
|
|
243
|
+
this.logger,
|
|
209
244
|
);
|
|
210
245
|
};
|
|
211
246
|
|
|
@@ -236,7 +271,7 @@ export class StreamSfuClient {
|
|
|
236
271
|
clearInterval(this.keepAliveInterval);
|
|
237
272
|
}
|
|
238
273
|
this.keepAliveInterval = setInterval(() => {
|
|
239
|
-
|
|
274
|
+
this.logger('info', 'Sending healthCheckRequest to SFU');
|
|
240
275
|
const message = SfuRequest.create({
|
|
241
276
|
requestPayload: {
|
|
242
277
|
oneofKind: 'healthCheckRequest',
|
|
@@ -258,7 +293,7 @@ export class StreamSfuClient {
|
|
|
258
293
|
new Date().getTime() - this.lastMessageTimestamp.getTime();
|
|
259
294
|
|
|
260
295
|
if (timeSinceLastMessage > this.unhealthyTimeoutInMs) {
|
|
261
|
-
|
|
296
|
+
this.logger('error', 'SFU connection unhealthy, closing');
|
|
262
297
|
this.close(
|
|
263
298
|
4001,
|
|
264
299
|
`SFU connection unhealthy. Didn't receive any healthcheck messages for ${this.unhealthyTimeoutInMs}ms`,
|
|
@@ -296,6 +331,7 @@ const MAX_RETRIES = 5;
|
|
|
296
331
|
*/
|
|
297
332
|
const retryable = async <I extends object, O extends SfuResponseWithError>(
|
|
298
333
|
rpc: () => UnaryCall<I, O>,
|
|
334
|
+
logger: Logger,
|
|
299
335
|
) => {
|
|
300
336
|
let retryAttempt = 0;
|
|
301
337
|
let rpcCallResult: FinishedUnaryCall<I, O>;
|
|
@@ -306,10 +342,15 @@ const retryable = async <I extends object, O extends SfuResponseWithError>(
|
|
|
306
342
|
}
|
|
307
343
|
|
|
308
344
|
rpcCallResult = await rpc();
|
|
345
|
+
logger(
|
|
346
|
+
'info',
|
|
347
|
+
`SFU RPC response received for ${rpcCallResult.method.name}`,
|
|
348
|
+
);
|
|
349
|
+
logger('debug', `Response payload`, rpcCallResult);
|
|
309
350
|
|
|
310
351
|
// if the RPC call failed, log the error and retry
|
|
311
352
|
if (rpcCallResult.response.error) {
|
|
312
|
-
|
|
353
|
+
logger('error', 'SFU RPC Error:', rpcCallResult.response.error);
|
|
313
354
|
}
|
|
314
355
|
retryAttempt++;
|
|
315
356
|
} while (
|