@stream-io/video-client 0.0.27 → 0.0.29
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 +2517 -1760
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +2537 -1758
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +2517 -1760
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +7 -8
- package/dist/src/StreamSfuClient.d.ts +23 -10
- package/dist/src/StreamVideoClient.d.ts +1 -4
- package/dist/src/client-details.d.ts +2 -1
- package/dist/src/coordinator/connection/types.d.ts +2 -2
- package/dist/src/coordinator/connection/utils.d.ts +1 -0
- package/dist/src/events/internal.d.ts +4 -0
- package/dist/src/gen/coordinator/index.d.ts +6 -0
- package/dist/src/gen/google/protobuf/struct.d.ts +8 -15
- package/dist/src/gen/google/protobuf/timestamp.d.ts +2 -9
- package/dist/src/gen/video/sfu/event/events.d.ts +121 -1
- package/dist/src/gen/video/sfu/models/models.d.ts +38 -1
- package/dist/src/gen/video/sfu/signal_rpc/signal.client.d.ts +3 -14
- package/dist/src/gen/video/sfu/signal_rpc/signal.d.ts +4 -12
- package/dist/src/logger.d.ts +4 -2
- package/dist/src/rtc/Dispatcher.d.ts +1 -2
- package/dist/src/rtc/{publisher.d.ts → Publisher.d.ts} +49 -15
- package/dist/src/rtc/Subscriber.d.ts +58 -0
- package/dist/src/rtc/__tests__/Subscriber.test.d.ts +1 -0
- package/dist/src/rtc/flows/join.d.ts +8 -1
- package/dist/src/rtc/index.d.ts +2 -2
- package/dist/src/rtc/signal.d.ts +1 -0
- package/dist/src/stats/state-store-stats-reporter.d.ts +3 -4
- package/dist/src/store/CallState.d.ts +10 -0
- package/package.json +3 -1
- package/src/Call.ts +218 -214
- package/src/StreamSfuClient.ts +48 -21
- package/src/StreamVideoClient.ts +7 -24
- package/src/client-details.ts +33 -1
- package/src/coordinator/connection/client.ts +6 -8
- package/src/coordinator/connection/types.ts +2 -3
- package/src/coordinator/connection/utils.ts +1 -0
- package/src/events/call.ts +0 -1
- package/src/events/callEventHandlers.ts +2 -0
- package/src/events/internal.ts +20 -0
- package/src/events/sessions.ts +0 -1
- package/src/gen/coordinator/index.ts +6 -0
- package/src/gen/google/protobuf/struct.ts +541 -333
- package/src/gen/google/protobuf/timestamp.ts +214 -148
- package/src/gen/video/sfu/event/events.ts +353 -3
- package/src/gen/video/sfu/models/models.ts +37 -0
- package/src/gen/video/sfu/signal_rpc/signal.client.ts +160 -94
- package/src/gen/video/sfu/signal_rpc/signal.ts +1214 -731
- package/src/logger.ts +43 -30
- package/src/rtc/Dispatcher.ts +5 -9
- package/src/rtc/{publisher.ts → Publisher.ts} +245 -111
- package/src/rtc/Subscriber.ts +304 -0
- package/src/rtc/__tests__/{publisher.test.ts → Publisher.test.ts} +77 -9
- package/src/rtc/__tests__/Subscriber.test.ts +121 -0
- package/src/rtc/__tests__/mocks/webrtc.mocks.ts +20 -0
- package/src/rtc/flows/join.ts +43 -2
- package/src/rtc/index.ts +2 -2
- package/src/rtc/signal.ts +6 -5
- package/src/rtc/videoLayers.ts +1 -4
- package/src/stats/state-store-stats-reporter.ts +3 -5
- package/src/store/CallState.ts +20 -0
- package/src/types.ts +0 -1
- package/dist/src/rtc/subscriber.d.ts +0 -9
- package/src/rtc/subscriber.ts +0 -107
- /package/dist/src/rtc/__tests__/{publisher.test.d.ts → Publisher.test.d.ts} +0 -0
package/src/Call.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { StreamSfuClient } from './StreamSfuClient';
|
|
2
2
|
import {
|
|
3
|
-
createSubscriber,
|
|
4
3
|
Dispatcher,
|
|
5
4
|
getGenericSdp,
|
|
6
5
|
isSfuEvent,
|
|
7
6
|
Publisher,
|
|
8
7
|
SfuEventKinds,
|
|
9
8
|
SfuEventListener,
|
|
9
|
+
Subscriber,
|
|
10
10
|
} from './rtc';
|
|
11
11
|
import { muteTypeToTrackType } from './rtc/helpers/tracks';
|
|
12
|
-
import {
|
|
12
|
+
import { GoAwayReason, TrackType } from './gen/video/sfu/models/models';
|
|
13
13
|
import {
|
|
14
14
|
registerEventHandlers,
|
|
15
15
|
registerRingingCallEventHandlers,
|
|
@@ -57,7 +57,7 @@ import {
|
|
|
57
57
|
UpdateUserPermissionsRequest,
|
|
58
58
|
UpdateUserPermissionsResponse,
|
|
59
59
|
} from './gen/coordinator';
|
|
60
|
-
import { join } from './rtc/flows/join';
|
|
60
|
+
import { join, reconcileParticipantLocalState } from './rtc/flows/join';
|
|
61
61
|
import {
|
|
62
62
|
CallConstructor,
|
|
63
63
|
CallLeaveOptions,
|
|
@@ -73,7 +73,6 @@ import {
|
|
|
73
73
|
BehaviorSubject,
|
|
74
74
|
debounce,
|
|
75
75
|
map,
|
|
76
|
-
of,
|
|
77
76
|
pairwise,
|
|
78
77
|
Subject,
|
|
79
78
|
takeWhile,
|
|
@@ -81,7 +80,7 @@ import {
|
|
|
81
80
|
timer,
|
|
82
81
|
} from 'rxjs';
|
|
83
82
|
import { TrackSubscriptionDetails } from './gen/video/sfu/signal_rpc/signal';
|
|
84
|
-
import { JoinResponse } from './gen/video/sfu/event/events';
|
|
83
|
+
import { JoinResponse, Migration } from './gen/video/sfu/event/events';
|
|
85
84
|
import { Timestamp } from './gen/google/protobuf/timestamp';
|
|
86
85
|
import {
|
|
87
86
|
createStatsReporter,
|
|
@@ -103,8 +102,7 @@ import {
|
|
|
103
102
|
Logger,
|
|
104
103
|
StreamCallEvent,
|
|
105
104
|
} from './coordinator/connection/types';
|
|
106
|
-
import {
|
|
107
|
-
import { getDeviceInfo, getOSInfo, getSdkInfo } from './client-details';
|
|
105
|
+
import { getClientDetails } from './client-details';
|
|
108
106
|
import { isReactNative } from './helpers/platforms';
|
|
109
107
|
import { getLogger } from './logger';
|
|
110
108
|
|
|
@@ -170,12 +168,12 @@ export class Call {
|
|
|
170
168
|
*/
|
|
171
169
|
private readonly dispatcher = new Dispatcher();
|
|
172
170
|
|
|
173
|
-
private subscriber?:
|
|
171
|
+
private subscriber?: Subscriber;
|
|
174
172
|
private publisher?: Publisher;
|
|
175
|
-
private trackSubscriptionsSubject = new
|
|
176
|
-
type
|
|
173
|
+
private trackSubscriptionsSubject = new BehaviorSubject<{
|
|
174
|
+
type: DebounceType;
|
|
177
175
|
data: TrackSubscriptionDetails[];
|
|
178
|
-
}>();
|
|
176
|
+
}>({ type: DebounceType.MEDIUM, data: [] });
|
|
179
177
|
|
|
180
178
|
private statsReporter?: StatsReporter;
|
|
181
179
|
private dropTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -223,7 +221,7 @@ export class Call {
|
|
|
223
221
|
this.streamClient = streamClient;
|
|
224
222
|
this.clientStore = clientStore;
|
|
225
223
|
this.streamClientBasePath = `/call/${this.type}/${this.id}`;
|
|
226
|
-
this.logger = getLogger(['
|
|
224
|
+
this.logger = getLogger(['Call']);
|
|
227
225
|
|
|
228
226
|
const callTypeConfig = CallTypes.get(type);
|
|
229
227
|
const participantSorter =
|
|
@@ -247,7 +245,7 @@ export class Call {
|
|
|
247
245
|
this.leaveCallHooks.push(
|
|
248
246
|
createSubscription(
|
|
249
247
|
this.trackSubscriptionsSubject.pipe(
|
|
250
|
-
debounce((v) =>
|
|
248
|
+
debounce((v) => timer(v.type)),
|
|
251
249
|
map((v) => v.data),
|
|
252
250
|
),
|
|
253
251
|
(subscriptions) => this.sfuClient?.updateSubscriptions(subscriptions),
|
|
@@ -410,7 +408,7 @@ export class Call {
|
|
|
410
408
|
this.subscriber?.close();
|
|
411
409
|
this.subscriber = undefined;
|
|
412
410
|
|
|
413
|
-
this.publisher?.
|
|
411
|
+
this.publisher?.close();
|
|
414
412
|
this.publisher = undefined;
|
|
415
413
|
|
|
416
414
|
this.sfuClient?.close();
|
|
@@ -453,21 +451,6 @@ export class Call {
|
|
|
453
451
|
return this.state.metadata?.created_by.id === this.currentUserId;
|
|
454
452
|
}
|
|
455
453
|
|
|
456
|
-
private waitForJoinResponse = (timeout: number = 5000) =>
|
|
457
|
-
new Promise<JoinResponse>((resolve, reject) => {
|
|
458
|
-
const unsubscribe = this.on('joinResponse', (event) => {
|
|
459
|
-
if (event.eventPayload.oneofKind !== 'joinResponse') return;
|
|
460
|
-
clearTimeout(timeoutId);
|
|
461
|
-
unsubscribe();
|
|
462
|
-
resolve(event.eventPayload.joinResponse);
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
const timeoutId = setTimeout(() => {
|
|
466
|
-
unsubscribe();
|
|
467
|
-
reject(new Error('Waiting for "joinResponse" has timed out'));
|
|
468
|
-
}, timeout);
|
|
469
|
-
});
|
|
470
|
-
|
|
471
454
|
/**
|
|
472
455
|
* Loads the information about the call.
|
|
473
456
|
*
|
|
@@ -576,11 +559,8 @@ export class Call {
|
|
|
576
559
|
* @returns a promise which resolves once the call join-flow has finished.
|
|
577
560
|
*/
|
|
578
561
|
join = async (data?: JoinCallData) => {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
this.state.callingState,
|
|
582
|
-
)
|
|
583
|
-
) {
|
|
562
|
+
const callingState = this.state.callingState;
|
|
563
|
+
if ([CallingState.JOINED, CallingState.JOINING].includes(callingState)) {
|
|
584
564
|
this.logger(
|
|
585
565
|
'warn',
|
|
586
566
|
'Join method called twice, you should only call this once',
|
|
@@ -588,13 +568,13 @@ export class Call {
|
|
|
588
568
|
throw new Error(`Illegal State: Already joined.`);
|
|
589
569
|
}
|
|
590
570
|
|
|
591
|
-
if (
|
|
571
|
+
if (callingState === CallingState.LEFT) {
|
|
592
572
|
throw new Error(
|
|
593
573
|
'Illegal State: Cannot join already left call. Create a new Call instance to join a call.',
|
|
594
574
|
);
|
|
595
575
|
}
|
|
596
576
|
|
|
597
|
-
const
|
|
577
|
+
const isMigrating = callingState === CallingState.MIGRATING;
|
|
598
578
|
this.state.setCallingState(CallingState.JOINING);
|
|
599
579
|
this.logger('debug', 'Starting join flow');
|
|
600
580
|
|
|
@@ -625,58 +605,87 @@ export class Call {
|
|
|
625
605
|
}
|
|
626
606
|
} catch (error) {
|
|
627
607
|
// restore the previous call state if the join-flow fails
|
|
628
|
-
this.state.setCallingState(
|
|
608
|
+
this.state.setCallingState(callingState);
|
|
629
609
|
throw error;
|
|
630
610
|
}
|
|
631
611
|
|
|
632
612
|
// FIXME OL: remove once cascading is implemented
|
|
633
|
-
|
|
634
|
-
let sfuWsUrl = sfuServer.ws_endpoint;
|
|
635
|
-
if (
|
|
636
|
-
typeof window !== 'undefined' &&
|
|
637
|
-
window.location &&
|
|
638
|
-
window.location.search
|
|
639
|
-
) {
|
|
613
|
+
if (typeof window !== 'undefined' && window.location.search) {
|
|
640
614
|
const params = new URLSearchParams(window.location.search);
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
sfuWsUrl = sfuWsUrlParam || sfuServer.ws_endpoint;
|
|
615
|
+
sfuServer.url = params.get('sfuUrl') || sfuServer.url;
|
|
616
|
+
sfuServer.ws_endpoint = params.get('sfuWsUrl') || sfuServer.ws_endpoint;
|
|
617
|
+
sfuServer.edge_name = params.get('sfuUrl') || sfuServer.edge_name;
|
|
645
618
|
}
|
|
646
619
|
|
|
647
|
-
const
|
|
620
|
+
const previousSfuClient = this.sfuClient;
|
|
648
621
|
const sfuClient = (this.sfuClient = new StreamSfuClient({
|
|
649
622
|
dispatcher: this.dispatcher,
|
|
650
|
-
|
|
651
|
-
wsEndpoint: sfuWsUrl,
|
|
623
|
+
sfuServer,
|
|
652
624
|
token: sfuToken,
|
|
653
|
-
sessionId:
|
|
625
|
+
sessionId: previousSfuClient?.sessionId,
|
|
654
626
|
}));
|
|
655
627
|
|
|
656
628
|
/**
|
|
657
629
|
* A closure which hides away the re-connection logic.
|
|
658
630
|
*/
|
|
659
|
-
const rejoin = async () => {
|
|
660
|
-
this.logger(
|
|
661
|
-
'debug',
|
|
662
|
-
`Rejoining call ${this.cid} (${this.reconnectAttempts})...`,
|
|
663
|
-
);
|
|
631
|
+
const rejoin = async ({ migrate = false } = {}) => {
|
|
664
632
|
this.reconnectAttempts++;
|
|
665
|
-
this.state.setCallingState(
|
|
633
|
+
this.state.setCallingState(
|
|
634
|
+
migrate ? CallingState.MIGRATING : CallingState.RECONNECTING,
|
|
635
|
+
);
|
|
636
|
+
|
|
637
|
+
if (migrate) {
|
|
638
|
+
this.logger(
|
|
639
|
+
'debug',
|
|
640
|
+
`[Migration]: migrating call ${this.cid} away from ${sfuServer.edge_name}`,
|
|
641
|
+
);
|
|
642
|
+
sfuClient.isMigratingAway = true;
|
|
643
|
+
} else {
|
|
644
|
+
this.logger(
|
|
645
|
+
'debug',
|
|
646
|
+
`[Rejoin]: Rejoining call ${this.cid} (${this.reconnectAttempts})...`,
|
|
647
|
+
);
|
|
648
|
+
}
|
|
666
649
|
|
|
667
650
|
// take a snapshot of the current "local participant" state
|
|
668
651
|
// we'll need it for restoring the previous publishing state later
|
|
669
652
|
const localParticipant = this.state.localParticipant;
|
|
670
653
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
654
|
+
const disconnectFromPreviousSfu = () => {
|
|
655
|
+
if (!migrate) {
|
|
656
|
+
this.subscriber?.close();
|
|
657
|
+
this.subscriber = undefined;
|
|
658
|
+
this.publisher?.close({ stopTracks: false });
|
|
659
|
+
this.publisher = undefined;
|
|
660
|
+
this.statsReporter?.stop();
|
|
661
|
+
this.statsReporter = undefined;
|
|
662
|
+
}
|
|
663
|
+
previousSfuClient?.close(); // clean up previous connection
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
if (!migrate) {
|
|
667
|
+
// in migration or recovery scenarios, we don't want to
|
|
668
|
+
// wait before attempting to reconnect to an SFU server
|
|
669
|
+
await sleep(retryInterval(this.reconnectAttempts));
|
|
670
|
+
disconnectFromPreviousSfu();
|
|
671
|
+
}
|
|
672
|
+
await this.join({
|
|
673
|
+
...data,
|
|
674
|
+
...(migrate && { migrating_from: sfuServer.edge_name }),
|
|
675
|
+
});
|
|
675
676
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
677
|
+
if (migrate) {
|
|
678
|
+
disconnectFromPreviousSfu();
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
this.logger(
|
|
682
|
+
'info',
|
|
683
|
+
`[Rejoin]: Attempt ${this.reconnectAttempts} successful!`,
|
|
684
|
+
);
|
|
685
|
+
// we shouldn't be republishing the streams if we're migrating
|
|
686
|
+
// as the underlying peer connection will take care of it as part
|
|
687
|
+
// of the ice-restart process
|
|
688
|
+
if (localParticipant && !isReactNative() && !migrate) {
|
|
680
689
|
const {
|
|
681
690
|
audioStream,
|
|
682
691
|
videoStream,
|
|
@@ -688,7 +697,10 @@ export class Call {
|
|
|
688
697
|
if (videoStream) await this.publishVideoStream(videoStream);
|
|
689
698
|
if (screenShare) await this.publishScreenShareStream(screenShare);
|
|
690
699
|
}
|
|
691
|
-
this.logger(
|
|
700
|
+
this.logger(
|
|
701
|
+
'info',
|
|
702
|
+
`[Rejoin]: State restored. Attempt: ${this.reconnectAttempts}`,
|
|
703
|
+
);
|
|
692
704
|
};
|
|
693
705
|
|
|
694
706
|
this.rejoinPromise = rejoin;
|
|
@@ -697,24 +709,57 @@ export class Call {
|
|
|
697
709
|
// - SFU crash or restart
|
|
698
710
|
// - network change
|
|
699
711
|
sfuClient.signalReady.then(() => {
|
|
712
|
+
// register a handler for the "goAway" event
|
|
713
|
+
const unregisterGoAway = this.dispatcher.on('goAway', (event) => {
|
|
714
|
+
if (event.eventPayload.oneofKind !== 'goAway') return;
|
|
715
|
+
const { reason } = event.eventPayload.goAway;
|
|
716
|
+
this.logger(
|
|
717
|
+
'info',
|
|
718
|
+
`[Migration]: Going away from SFU... Reason: ${GoAwayReason[reason]}`,
|
|
719
|
+
);
|
|
720
|
+
rejoin({ migrate: true }).catch((err) => {
|
|
721
|
+
this.logger(
|
|
722
|
+
'warn',
|
|
723
|
+
`[Migration]: Failed to migrate to another SFU.`,
|
|
724
|
+
err,
|
|
725
|
+
);
|
|
726
|
+
});
|
|
727
|
+
});
|
|
728
|
+
|
|
700
729
|
sfuClient.signalWs.addEventListener('close', (e) => {
|
|
730
|
+
// unregister the "goAway" handler, as we won't need it anymore for this connection.
|
|
731
|
+
// the upcoming re-join will register a new handler anyway
|
|
732
|
+
unregisterGoAway();
|
|
701
733
|
// do nothing if the connection was closed on purpose
|
|
702
734
|
if (e.code === KnownCodes.WS_CLOSED_SUCCESS) return;
|
|
703
735
|
// do nothing if the connection was closed because of a policy violation
|
|
704
736
|
// e.g., the user has been blocked by an admin or moderator
|
|
705
737
|
if (e.code === KnownCodes.WS_POLICY_VIOLATION) return;
|
|
706
|
-
//
|
|
738
|
+
// When the SFU is being shut down, it sends a goAway message.
|
|
739
|
+
// While we migrate to another SFU, we might have the WS connection
|
|
740
|
+
// to the old SFU closed abruptly. In this case, we don't want
|
|
741
|
+
// to reconnect to the old SFU, but rather to the new one.
|
|
742
|
+
if (
|
|
743
|
+
e.code === KnownCodes.WS_CLOSED_ABRUPTLY &&
|
|
744
|
+
sfuClient.isMigratingAway
|
|
745
|
+
)
|
|
746
|
+
return;
|
|
747
|
+
// do nothing for react-native as it is handled by SDK
|
|
707
748
|
if (isReactNative()) return;
|
|
708
749
|
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
709
|
-
rejoin().catch(() => {
|
|
750
|
+
rejoin().catch((err) => {
|
|
710
751
|
this.logger(
|
|
711
752
|
'error',
|
|
712
|
-
`Rejoin failed for ${this.reconnectAttempts} times. Giving up.`,
|
|
753
|
+
`[Rejoin]: Rejoin failed for ${this.reconnectAttempts} times. Giving up.`,
|
|
754
|
+
err,
|
|
713
755
|
);
|
|
714
756
|
this.state.setCallingState(CallingState.RECONNECTING_FAILED);
|
|
715
757
|
});
|
|
716
758
|
} else {
|
|
717
|
-
this.logger(
|
|
759
|
+
this.logger(
|
|
760
|
+
'error',
|
|
761
|
+
'[Rejoin]: Reconnect attempts exceeded. Giving up...',
|
|
762
|
+
);
|
|
718
763
|
this.state.setCallingState(CallingState.RECONNECTING_FAILED);
|
|
719
764
|
}
|
|
720
765
|
});
|
|
@@ -725,18 +770,19 @@ export class Call {
|
|
|
725
770
|
if (typeof window !== 'undefined' && window.addEventListener) {
|
|
726
771
|
const handleOnOffline = () => {
|
|
727
772
|
window.removeEventListener('offline', handleOnOffline);
|
|
728
|
-
this.logger('warn', '
|
|
773
|
+
this.logger('warn', '[Rejoin]: Going offline...');
|
|
729
774
|
this.state.setCallingState(CallingState.OFFLINE);
|
|
730
775
|
};
|
|
731
776
|
|
|
732
777
|
const handleOnOnline = () => {
|
|
733
778
|
window.removeEventListener('online', handleOnOnline);
|
|
734
779
|
if (this.state.callingState === CallingState.OFFLINE) {
|
|
735
|
-
this.logger('info', '
|
|
736
|
-
rejoin().catch(() => {
|
|
780
|
+
this.logger('info', '[Rejoin]: Going online...');
|
|
781
|
+
rejoin().catch((err) => {
|
|
737
782
|
this.logger(
|
|
738
783
|
'error',
|
|
739
|
-
`Rejoin failed for ${this.reconnectAttempts} times. Giving up.`,
|
|
784
|
+
`[Rejoin]: Rejoin failed for ${this.reconnectAttempts} times. Giving up.`,
|
|
785
|
+
err,
|
|
740
786
|
);
|
|
741
787
|
this.state.setCallingState(CallingState.RECONNECTING_FAILED);
|
|
742
788
|
});
|
|
@@ -753,57 +799,39 @@ export class Call {
|
|
|
753
799
|
);
|
|
754
800
|
}
|
|
755
801
|
|
|
756
|
-
this.subscriber
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
802
|
+
if (!this.subscriber) {
|
|
803
|
+
this.subscriber = new Subscriber({
|
|
804
|
+
sfuClient,
|
|
805
|
+
dispatcher: this.dispatcher,
|
|
806
|
+
state: this.state,
|
|
807
|
+
connectionConfig,
|
|
808
|
+
});
|
|
809
|
+
}
|
|
762
810
|
|
|
763
811
|
const audioSettings = this.data?.settings.audio;
|
|
764
812
|
const isDtxEnabled = !!audioSettings?.opus_dtx_enabled;
|
|
765
813
|
const isRedEnabled = !!audioSettings?.redundant_coding_enabled;
|
|
766
|
-
this.publisher = new Publisher({
|
|
767
|
-
sfuClient,
|
|
768
|
-
state: this.state,
|
|
769
|
-
connectionConfig,
|
|
770
|
-
isDtxEnabled,
|
|
771
|
-
isRedEnabled,
|
|
772
|
-
preferredVideoCodec: this.streamClient.options.preferredVideoCodec,
|
|
773
|
-
});
|
|
774
814
|
|
|
775
|
-
this.
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
815
|
+
if (!this.publisher) {
|
|
816
|
+
this.publisher = new Publisher({
|
|
817
|
+
sfuClient,
|
|
818
|
+
state: this.state,
|
|
819
|
+
connectionConfig,
|
|
820
|
+
isDtxEnabled,
|
|
821
|
+
isRedEnabled,
|
|
822
|
+
preferredVideoCodec: this.streamClient.options.preferredVideoCodec,
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (!isMigrating) {
|
|
827
|
+
this.statsReporter = createStatsReporter({
|
|
828
|
+
subscriber: this.subscriber,
|
|
829
|
+
publisher: this.publisher,
|
|
830
|
+
state: this.state,
|
|
831
|
+
});
|
|
832
|
+
}
|
|
781
833
|
|
|
782
834
|
try {
|
|
783
|
-
const clientDetails: ClientDetails = {};
|
|
784
|
-
if (isReactNative()) {
|
|
785
|
-
// Since RN doesn't support web, sharing browser info is not required
|
|
786
|
-
clientDetails.os = getOSInfo();
|
|
787
|
-
clientDetails.device = getDeviceInfo();
|
|
788
|
-
} else {
|
|
789
|
-
const details = new UAParser(navigator.userAgent).getResult();
|
|
790
|
-
clientDetails.browser = {
|
|
791
|
-
name: details.browser.name || navigator.userAgent,
|
|
792
|
-
version: details.browser.version || '',
|
|
793
|
-
};
|
|
794
|
-
clientDetails.os = {
|
|
795
|
-
name: details.os.name || '',
|
|
796
|
-
version: details.os.version || '',
|
|
797
|
-
architecture: details.cpu.architecture || '',
|
|
798
|
-
};
|
|
799
|
-
clientDetails.device = {
|
|
800
|
-
name: `${details.device.vendor || ''} ${details.device.model || ''} ${
|
|
801
|
-
details.device.type || ''
|
|
802
|
-
}`,
|
|
803
|
-
version: '',
|
|
804
|
-
};
|
|
805
|
-
}
|
|
806
|
-
clientDetails.sdk = getSdkInfo();
|
|
807
835
|
// 1. wait for the signal server to be ready before sending "joinRequest"
|
|
808
836
|
sfuClient.signalReady
|
|
809
837
|
.catch((err) => this.logger('error', 'Signal ready failed', err))
|
|
@@ -818,31 +846,52 @@ export class Call {
|
|
|
818
846
|
),
|
|
819
847
|
)
|
|
820
848
|
.then((sdp) => {
|
|
821
|
-
const
|
|
849
|
+
const subscriptions = getCurrentValue(this.trackSubscriptionsSubject);
|
|
850
|
+
const migration: Migration | undefined = isMigrating
|
|
851
|
+
? {
|
|
852
|
+
fromSfuId: data?.migrating_from || '',
|
|
853
|
+
subscriptions: subscriptions.data || [],
|
|
854
|
+
announcedTracks: this.publisher?.getCurrentTrackInfos() || [],
|
|
855
|
+
}
|
|
856
|
+
: undefined;
|
|
857
|
+
|
|
858
|
+
return sfuClient.join({
|
|
822
859
|
subscriberSdp: sdp || '',
|
|
823
|
-
clientDetails,
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
this.logger('debug', 'Join request payload', joinRequest);
|
|
827
|
-
sfuClient.join(joinRequest);
|
|
860
|
+
clientDetails: getClientDetails(),
|
|
861
|
+
migration,
|
|
862
|
+
});
|
|
828
863
|
});
|
|
829
864
|
|
|
830
865
|
// 2. in parallel, wait for the SFU to send us the "joinResponse"
|
|
831
866
|
// this will throw an error if the SFU rejects the join request or
|
|
832
867
|
// fails to respond in time
|
|
833
868
|
const { callState } = await this.waitForJoinResponse();
|
|
869
|
+
if (isMigrating) {
|
|
870
|
+
await this.subscriber.migrateTo(sfuClient, connectionConfig);
|
|
871
|
+
await this.publisher.migrateTo(sfuClient, connectionConfig);
|
|
872
|
+
}
|
|
834
873
|
const currentParticipants = callState?.participants || [];
|
|
835
874
|
const participantCount = callState?.participantCount;
|
|
836
875
|
const startedAt = callState?.startedAt
|
|
837
876
|
? Timestamp.toDate(callState.startedAt)
|
|
838
877
|
: new Date();
|
|
839
|
-
this.state.setParticipants(
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
878
|
+
this.state.setParticipants(() => {
|
|
879
|
+
const participantLookup = this.state.getParticipantLookupBySessionId();
|
|
880
|
+
return currentParticipants.map((p) => {
|
|
881
|
+
const participant: StreamVideoParticipant = Object.assign(p, {
|
|
882
|
+
isLocalParticipant: p.sessionId === sfuClient.sessionId,
|
|
883
|
+
viewportVisibilityState: VisibilityState.UNKNOWN,
|
|
884
|
+
});
|
|
885
|
+
// We need to preserve some of the local state of the participant
|
|
886
|
+
// (e.g. videoDimension, visibilityState, pinnedAt, etc.)
|
|
887
|
+
// as it doesn't exist on the server.
|
|
888
|
+
const existingParticipant = participantLookup[p.sessionId];
|
|
889
|
+
return reconcileParticipantLocalState(
|
|
890
|
+
participant,
|
|
891
|
+
existingParticipant,
|
|
892
|
+
);
|
|
893
|
+
});
|
|
894
|
+
});
|
|
846
895
|
this.state.setParticipantCount(participantCount?.total || 0);
|
|
847
896
|
this.state.setAnonymousParticipantCount(participantCount?.anonymous || 0);
|
|
848
897
|
this.state.setStartedAt(startedAt);
|
|
@@ -853,12 +902,20 @@ export class Call {
|
|
|
853
902
|
} catch (err) {
|
|
854
903
|
// join failed, try to rejoin
|
|
855
904
|
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
905
|
+
this.logger(
|
|
906
|
+
'error',
|
|
907
|
+
`[Rejoin]: Rejoin ${this.reconnectAttempts} failed.`,
|
|
908
|
+
err,
|
|
909
|
+
);
|
|
856
910
|
await rejoin();
|
|
857
|
-
this.logger(
|
|
911
|
+
this.logger(
|
|
912
|
+
'info',
|
|
913
|
+
`[Rejoin]: Rejoin ${this.reconnectAttempts} successful!`,
|
|
914
|
+
);
|
|
858
915
|
} else {
|
|
859
916
|
this.logger(
|
|
860
917
|
'error',
|
|
861
|
-
`Rejoin failed for ${this.reconnectAttempts} times. Giving up.`,
|
|
918
|
+
`[Rejoin]: Rejoin failed for ${this.reconnectAttempts} times. Giving up.`,
|
|
862
919
|
);
|
|
863
920
|
this.state.setCallingState(CallingState.RECONNECTING_FAILED);
|
|
864
921
|
throw new Error('Join failed');
|
|
@@ -866,6 +923,30 @@ export class Call {
|
|
|
866
923
|
}
|
|
867
924
|
};
|
|
868
925
|
|
|
926
|
+
private waitForJoinResponse = (timeout: number = 5000) => {
|
|
927
|
+
return new Promise<JoinResponse>((resolve, reject) => {
|
|
928
|
+
const unsubscribe = this.on('joinResponse', (event) => {
|
|
929
|
+
if (event.eventPayload.oneofKind !== 'joinResponse') return;
|
|
930
|
+
clearTimeout(timeoutId);
|
|
931
|
+
unsubscribe();
|
|
932
|
+
resolve(event.eventPayload.joinResponse);
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
const timeoutId = setTimeout(() => {
|
|
936
|
+
unsubscribe();
|
|
937
|
+
reject(new Error('Waiting for "joinResponse" has timed out'));
|
|
938
|
+
}, timeout);
|
|
939
|
+
});
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
private assertCallJoined = () => {
|
|
943
|
+
return new Promise<void>((resolve) => {
|
|
944
|
+
this.state.callingState$
|
|
945
|
+
.pipe(takeWhile((state) => state !== CallingState.JOINED, true))
|
|
946
|
+
.subscribe(() => resolve());
|
|
947
|
+
});
|
|
948
|
+
};
|
|
949
|
+
|
|
869
950
|
/**
|
|
870
951
|
* Starts publishing the given video stream to the call.
|
|
871
952
|
* The stream will be stopped if the user changes an input device, or if the user leaves the call.
|
|
@@ -1151,81 +1232,6 @@ export class Call {
|
|
|
1151
1232
|
return this.publisher?.updateVideoPublishQuality(enabledRids);
|
|
1152
1233
|
};
|
|
1153
1234
|
|
|
1154
|
-
private handleOnTrack = (e: RTCTrackEvent) => {
|
|
1155
|
-
const [primaryStream] = e.streams;
|
|
1156
|
-
// example: `e3f6aaf8-b03d-4911-be36-83f47d37a76a:TRACK_TYPE_VIDEO`
|
|
1157
|
-
const [trackId, trackType] = primaryStream.id.split(':');
|
|
1158
|
-
this.logger('info', `Got remote ${trackType} track:`);
|
|
1159
|
-
this.logger('debug', `Track: `, e.track);
|
|
1160
|
-
const participantToUpdate = this.state.participants.find(
|
|
1161
|
-
(p) => p.trackLookupPrefix === trackId,
|
|
1162
|
-
);
|
|
1163
|
-
if (!participantToUpdate) {
|
|
1164
|
-
this.logger(
|
|
1165
|
-
'error',
|
|
1166
|
-
`'Received track for unknown participant: ${trackId}'`,
|
|
1167
|
-
e,
|
|
1168
|
-
);
|
|
1169
|
-
return;
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
e.track.addEventListener('mute', () => {
|
|
1173
|
-
this.logger(
|
|
1174
|
-
'info',
|
|
1175
|
-
`Track muted: ${participantToUpdate.userId} ${trackType}:${trackId}`,
|
|
1176
|
-
);
|
|
1177
|
-
});
|
|
1178
|
-
|
|
1179
|
-
e.track.addEventListener('unmute', () => {
|
|
1180
|
-
this.logger(
|
|
1181
|
-
'info',
|
|
1182
|
-
`Track unmuted: ${participantToUpdate.userId} ${trackType}:${trackId}`,
|
|
1183
|
-
);
|
|
1184
|
-
});
|
|
1185
|
-
|
|
1186
|
-
e.track.addEventListener('ended', () => {
|
|
1187
|
-
this.logger(
|
|
1188
|
-
'info',
|
|
1189
|
-
`Track ended: ${participantToUpdate.userId} ${trackType}:${trackId}`,
|
|
1190
|
-
);
|
|
1191
|
-
});
|
|
1192
|
-
|
|
1193
|
-
const streamKindProp = (
|
|
1194
|
-
{
|
|
1195
|
-
TRACK_TYPE_AUDIO: 'audioStream',
|
|
1196
|
-
TRACK_TYPE_VIDEO: 'videoStream',
|
|
1197
|
-
TRACK_TYPE_SCREEN_SHARE: 'screenShareStream',
|
|
1198
|
-
} as const
|
|
1199
|
-
)[trackType];
|
|
1200
|
-
|
|
1201
|
-
if (!streamKindProp) {
|
|
1202
|
-
this.logger('error', `Unknown track type: ${trackType}`);
|
|
1203
|
-
return;
|
|
1204
|
-
}
|
|
1205
|
-
const previousStream = participantToUpdate[streamKindProp];
|
|
1206
|
-
if (previousStream) {
|
|
1207
|
-
this.logger(
|
|
1208
|
-
'info',
|
|
1209
|
-
`Cleaning up previous remote tracks: ${e.track.kind}`,
|
|
1210
|
-
);
|
|
1211
|
-
previousStream.getTracks().forEach((t) => {
|
|
1212
|
-
t.stop();
|
|
1213
|
-
previousStream.removeTrack(t);
|
|
1214
|
-
});
|
|
1215
|
-
}
|
|
1216
|
-
this.state.updateParticipant(participantToUpdate.sessionId, {
|
|
1217
|
-
[streamKindProp]: primaryStream,
|
|
1218
|
-
});
|
|
1219
|
-
};
|
|
1220
|
-
|
|
1221
|
-
private assertCallJoined = () => {
|
|
1222
|
-
return new Promise<void>((resolve) => {
|
|
1223
|
-
this.state.callingState$
|
|
1224
|
-
.pipe(takeWhile((state) => state !== CallingState.JOINED, true))
|
|
1225
|
-
.subscribe(() => resolve());
|
|
1226
|
-
});
|
|
1227
|
-
};
|
|
1228
|
-
|
|
1229
1235
|
/**
|
|
1230
1236
|
* Sends a reaction to the other call participants.
|
|
1231
1237
|
*
|
|
@@ -1605,16 +1611,14 @@ export class Call {
|
|
|
1605
1611
|
};
|
|
1606
1612
|
|
|
1607
1613
|
/**
|
|
1608
|
-
* Sends
|
|
1614
|
+
* Sends a custom event to all call participants.
|
|
1609
1615
|
*
|
|
1610
1616
|
* @param event the event to send.
|
|
1611
1617
|
*/
|
|
1612
|
-
|
|
1613
|
-
event: SendEventRequest & { type: StreamCallEvent['type'] },
|
|
1614
|
-
) => {
|
|
1618
|
+
sendCustomEvent = async (payload: { [key: string]: any }) => {
|
|
1615
1619
|
return this.streamClient.post<SendEventResponse, SendEventRequest>(
|
|
1616
1620
|
`${this.streamClientBasePath}/event`,
|
|
1617
|
-
|
|
1621
|
+
{ custom: payload },
|
|
1618
1622
|
);
|
|
1619
1623
|
};
|
|
1620
1624
|
}
|