@stream-io/video-client 1.5.0-0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -230
- package/dist/index.browser.es.js +1498 -1963
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +1495 -1961
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +1498 -1963
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +9 -93
- package/dist/src/StreamSfuClient.d.ts +56 -72
- package/dist/src/StreamVideoClient.d.ts +10 -2
- package/dist/src/coordinator/connection/client.d.ts +4 -3
- package/dist/src/coordinator/connection/types.d.ts +1 -5
- package/dist/src/devices/InputMediaDeviceManager.d.ts +0 -4
- package/dist/src/devices/MicrophoneManager.d.ts +1 -1
- package/dist/src/events/callEventHandlers.d.ts +3 -1
- package/dist/src/events/internal.d.ts +0 -4
- package/dist/src/gen/video/sfu/event/events.d.ts +4 -106
- package/dist/src/gen/video/sfu/models/models.d.ts +65 -64
- package/dist/src/logger.d.ts +0 -1
- package/dist/src/rpc/createClient.d.ts +0 -2
- package/dist/src/rpc/index.d.ts +0 -1
- package/dist/src/rtc/Dispatcher.d.ts +1 -1
- package/dist/src/rtc/IceTrickleBuffer.d.ts +1 -0
- package/dist/src/rtc/Publisher.d.ts +25 -24
- package/dist/src/rtc/Subscriber.d.ts +11 -12
- package/dist/src/rtc/flows/join.d.ts +20 -0
- package/dist/src/rtc/helpers/tracks.d.ts +3 -3
- package/dist/src/rtc/signal.d.ts +1 -1
- package/dist/src/store/CallState.d.ts +2 -46
- package/package.json +3 -3
- package/src/Call.ts +562 -615
- package/src/StreamSfuClient.ts +246 -277
- package/src/StreamVideoClient.ts +65 -15
- package/src/coordinator/connection/client.ts +8 -25
- package/src/coordinator/connection/connection.ts +0 -1
- package/src/coordinator/connection/token_manager.ts +1 -1
- package/src/coordinator/connection/types.ts +0 -6
- package/src/devices/BrowserPermission.ts +1 -5
- package/src/devices/CameraManager.ts +1 -1
- package/src/devices/InputMediaDeviceManager.ts +3 -12
- package/src/devices/MicrophoneManager.ts +3 -3
- package/src/devices/devices.ts +1 -1
- package/src/events/__tests__/mutes.test.ts +13 -10
- package/src/events/__tests__/participant.test.ts +0 -75
- package/src/events/callEventHandlers.ts +7 -4
- package/src/events/internal.ts +3 -20
- package/src/events/mutes.ts +3 -5
- package/src/events/participant.ts +15 -48
- package/src/gen/video/sfu/event/events.ts +8 -451
- package/src/gen/video/sfu/models/models.ts +204 -211
- package/src/logger.ts +1 -3
- package/src/rpc/createClient.ts +0 -21
- package/src/rpc/index.ts +0 -1
- package/src/rtc/Dispatcher.ts +2 -6
- package/src/rtc/IceTrickleBuffer.ts +2 -2
- package/src/rtc/Publisher.ts +163 -127
- package/src/rtc/Subscriber.ts +155 -94
- package/src/rtc/__tests__/Publisher.test.ts +95 -18
- package/src/rtc/__tests__/Subscriber.test.ts +99 -63
- package/src/rtc/__tests__/videoLayers.test.ts +2 -2
- package/src/rtc/flows/join.ts +65 -0
- package/src/rtc/helpers/tracks.ts +7 -27
- package/src/rtc/signal.ts +3 -3
- package/src/rtc/videoLayers.ts +10 -1
- package/src/stats/SfuStatsReporter.ts +0 -1
- package/src/store/CallState.ts +2 -109
- package/src/store/__tests__/CallState.test.ts +37 -48
- package/dist/src/helpers/ensureExhausted.d.ts +0 -1
- package/dist/src/helpers/withResolvers.d.ts +0 -14
- package/dist/src/rpc/retryable.d.ts +0 -23
- package/dist/src/rtc/helpers/rtcConfiguration.d.ts +0 -2
- package/src/helpers/ensureExhausted.ts +0 -5
- package/src/helpers/withResolvers.ts +0 -43
- package/src/rpc/__tests__/retryable.test.ts +0 -72
- package/src/rpc/retryable.ts +0 -57
- package/src/rtc/helpers/rtcConfiguration.ts +0 -11
package/dist/index.es.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import 'webrtc-adapter';
|
|
2
2
|
import { MessageType, isJsonObject, typeofJsonValue, reflectionMergePartial, UnknownFieldHandler, WireType, PbLong } from '@protobuf-ts/runtime';
|
|
3
|
-
import { ServiceType, stackIntercept
|
|
3
|
+
import { ServiceType, stackIntercept } from '@protobuf-ts/runtime-rpc';
|
|
4
4
|
import axios, { AxiosHeaders } from 'axios';
|
|
5
5
|
export { AxiosError } from 'axios';
|
|
6
|
-
import { TwirpFetchTransport
|
|
6
|
+
import { TwirpFetchTransport } from '@protobuf-ts/twirp-transport';
|
|
7
7
|
import { ReplaySubject, combineLatest, BehaviorSubject, map as map$1, shareReplay, distinctUntilChanged, takeWhile, distinctUntilKeyChanged, fromEventPattern, startWith, concatMap, merge, from, fromEvent, debounceTime, pairwise, of, filter, debounce, timer } from 'rxjs';
|
|
8
8
|
import * as SDP from 'sdp-transform';
|
|
9
9
|
import { UAParser } from 'ua-parser-js';
|
|
10
|
+
import WebSocket from 'isomorphic-ws';
|
|
10
11
|
import https from 'https';
|
|
11
|
-
import WebSocket$1 from 'isomorphic-ws';
|
|
12
12
|
import { fromByteArray } from 'base64-js';
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -1015,10 +1015,6 @@ var CallEndedReason;
|
|
|
1015
1015
|
* @generated from protobuf enum value: CALL_ENDED_REASON_KICKED = 3;
|
|
1016
1016
|
*/
|
|
1017
1017
|
CallEndedReason[CallEndedReason["KICKED"] = 3] = "KICKED";
|
|
1018
|
-
/**
|
|
1019
|
-
* @generated from protobuf enum value: CALL_ENDED_REASON_SESSION_ENDED = 4;
|
|
1020
|
-
*/
|
|
1021
|
-
CallEndedReason[CallEndedReason["SESSION_ENDED"] = 4] = "SESSION_ENDED";
|
|
1022
1018
|
})(CallEndedReason || (CallEndedReason = {}));
|
|
1023
1019
|
/**
|
|
1024
1020
|
* WebsocketReconnectStrategy defines the ws strategies available for handling reconnections.
|
|
@@ -1032,7 +1028,7 @@ var WebsocketReconnectStrategy;
|
|
|
1032
1028
|
*/
|
|
1033
1029
|
WebsocketReconnectStrategy[WebsocketReconnectStrategy["UNSPECIFIED"] = 0] = "UNSPECIFIED";
|
|
1034
1030
|
/**
|
|
1035
|
-
* Sent after reaching the maximum reconnection attempts,
|
|
1031
|
+
* Sent after reaching the maximum reconnection attempts, leading to permanent disconnect.
|
|
1036
1032
|
*
|
|
1037
1033
|
* @generated from protobuf enum value: WEBSOCKET_RECONNECT_STRATEGY_DISCONNECT = 1;
|
|
1038
1034
|
*/
|
|
@@ -1045,18 +1041,25 @@ var WebsocketReconnectStrategy;
|
|
|
1045
1041
|
*/
|
|
1046
1042
|
WebsocketReconnectStrategy[WebsocketReconnectStrategy["FAST"] = 2] = "FAST";
|
|
1047
1043
|
/**
|
|
1048
|
-
* SDK should
|
|
1044
|
+
* SDK should drop existing pc instances and creates a fresh WebSocket connection,
|
|
1045
|
+
* ensuring a clean state for the reconnection.
|
|
1046
|
+
*
|
|
1047
|
+
* @generated from protobuf enum value: WEBSOCKET_RECONNECT_STRATEGY_CLEAN = 3;
|
|
1048
|
+
*/
|
|
1049
|
+
WebsocketReconnectStrategy[WebsocketReconnectStrategy["CLEAN"] = 3] = "CLEAN";
|
|
1050
|
+
/**
|
|
1051
|
+
* SDK should obtain new credentials from the coordinator, drops existing pc instances, and initializes
|
|
1049
1052
|
* a completely new WebSocket connection, ensuring a comprehensive reset.
|
|
1050
1053
|
*
|
|
1051
|
-
* @generated from protobuf enum value:
|
|
1054
|
+
* @generated from protobuf enum value: WEBSOCKET_RECONNECT_STRATEGY_FULL = 4;
|
|
1052
1055
|
*/
|
|
1053
|
-
WebsocketReconnectStrategy[WebsocketReconnectStrategy["
|
|
1056
|
+
WebsocketReconnectStrategy[WebsocketReconnectStrategy["FULL"] = 4] = "FULL";
|
|
1054
1057
|
/**
|
|
1055
1058
|
* SDK should migrate to a new SFU instance
|
|
1056
1059
|
*
|
|
1057
|
-
* @generated from protobuf enum value: WEBSOCKET_RECONNECT_STRATEGY_MIGRATE =
|
|
1060
|
+
* @generated from protobuf enum value: WEBSOCKET_RECONNECT_STRATEGY_MIGRATE = 5;
|
|
1058
1061
|
*/
|
|
1059
|
-
WebsocketReconnectStrategy[WebsocketReconnectStrategy["MIGRATE"] =
|
|
1062
|
+
WebsocketReconnectStrategy[WebsocketReconnectStrategy["MIGRATE"] = 5] = "MIGRATE";
|
|
1060
1063
|
})(WebsocketReconnectStrategy || (WebsocketReconnectStrategy = {}));
|
|
1061
1064
|
// @generated message type with reflection information, may provide speed optimized methods
|
|
1062
1065
|
class CallState$Type extends MessageType {
|
|
@@ -1858,7 +1861,6 @@ class TrackInfo$Type extends MessageType {
|
|
|
1858
1861
|
{ no: 7, name: 'dtx', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
|
|
1859
1862
|
{ no: 8, name: 'stereo', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
|
|
1860
1863
|
{ no: 9, name: 'red', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
|
|
1861
|
-
{ no: 10, name: 'muted', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
|
|
1862
1864
|
]);
|
|
1863
1865
|
}
|
|
1864
1866
|
create(value) {
|
|
@@ -1870,7 +1872,6 @@ class TrackInfo$Type extends MessageType {
|
|
|
1870
1872
|
message.dtx = false;
|
|
1871
1873
|
message.stereo = false;
|
|
1872
1874
|
message.red = false;
|
|
1873
|
-
message.muted = false;
|
|
1874
1875
|
if (value !== undefined)
|
|
1875
1876
|
reflectionMergePartial(this, message, value);
|
|
1876
1877
|
return message;
|
|
@@ -1901,9 +1902,6 @@ class TrackInfo$Type extends MessageType {
|
|
|
1901
1902
|
case /* bool red */ 9:
|
|
1902
1903
|
message.red = reader.bool();
|
|
1903
1904
|
break;
|
|
1904
|
-
case /* bool muted */ 10:
|
|
1905
|
-
message.muted = reader.bool();
|
|
1906
|
-
break;
|
|
1907
1905
|
default:
|
|
1908
1906
|
let u = options.readUnknownField;
|
|
1909
1907
|
if (u === 'throw')
|
|
@@ -1937,9 +1935,6 @@ class TrackInfo$Type extends MessageType {
|
|
|
1937
1935
|
/* bool red = 9; */
|
|
1938
1936
|
if (message.red !== false)
|
|
1939
1937
|
writer.tag(9, WireType.Varint).bool(message.red);
|
|
1940
|
-
/* bool muted = 10; */
|
|
1941
|
-
if (message.muted !== false)
|
|
1942
|
-
writer.tag(10, WireType.Varint).bool(message.muted);
|
|
1943
1938
|
let u = options.writeUnknownFields;
|
|
1944
1939
|
if (u !== false)
|
|
1945
1940
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
|
@@ -1951,6 +1946,108 @@ class TrackInfo$Type extends MessageType {
|
|
|
1951
1946
|
*/
|
|
1952
1947
|
const TrackInfo = new TrackInfo$Type();
|
|
1953
1948
|
// @generated message type with reflection information, may provide speed optimized methods
|
|
1949
|
+
class Call$Type extends MessageType {
|
|
1950
|
+
constructor() {
|
|
1951
|
+
super('stream.video.sfu.models.Call', [
|
|
1952
|
+
{ no: 1, name: 'type', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
|
|
1953
|
+
{ no: 2, name: 'id', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
|
|
1954
|
+
{
|
|
1955
|
+
no: 3,
|
|
1956
|
+
name: 'created_by_user_id',
|
|
1957
|
+
kind: 'scalar',
|
|
1958
|
+
T: 9 /*ScalarType.STRING*/,
|
|
1959
|
+
},
|
|
1960
|
+
{
|
|
1961
|
+
no: 4,
|
|
1962
|
+
name: 'host_user_id',
|
|
1963
|
+
kind: 'scalar',
|
|
1964
|
+
T: 9 /*ScalarType.STRING*/,
|
|
1965
|
+
},
|
|
1966
|
+
{ no: 5, name: 'custom', kind: 'message', T: () => Struct },
|
|
1967
|
+
{ no: 6, name: 'created_at', kind: 'message', T: () => Timestamp },
|
|
1968
|
+
{ no: 7, name: 'updated_at', kind: 'message', T: () => Timestamp },
|
|
1969
|
+
]);
|
|
1970
|
+
}
|
|
1971
|
+
create(value) {
|
|
1972
|
+
const message = globalThis.Object.create(this.messagePrototype);
|
|
1973
|
+
message.type = '';
|
|
1974
|
+
message.id = '';
|
|
1975
|
+
message.createdByUserId = '';
|
|
1976
|
+
message.hostUserId = '';
|
|
1977
|
+
if (value !== undefined)
|
|
1978
|
+
reflectionMergePartial(this, message, value);
|
|
1979
|
+
return message;
|
|
1980
|
+
}
|
|
1981
|
+
internalBinaryRead(reader, length, options, target) {
|
|
1982
|
+
let message = target ?? this.create(), end = reader.pos + length;
|
|
1983
|
+
while (reader.pos < end) {
|
|
1984
|
+
let [fieldNo, wireType] = reader.tag();
|
|
1985
|
+
switch (fieldNo) {
|
|
1986
|
+
case /* string type */ 1:
|
|
1987
|
+
message.type = reader.string();
|
|
1988
|
+
break;
|
|
1989
|
+
case /* string id */ 2:
|
|
1990
|
+
message.id = reader.string();
|
|
1991
|
+
break;
|
|
1992
|
+
case /* string created_by_user_id */ 3:
|
|
1993
|
+
message.createdByUserId = reader.string();
|
|
1994
|
+
break;
|
|
1995
|
+
case /* string host_user_id */ 4:
|
|
1996
|
+
message.hostUserId = reader.string();
|
|
1997
|
+
break;
|
|
1998
|
+
case /* google.protobuf.Struct custom */ 5:
|
|
1999
|
+
message.custom = Struct.internalBinaryRead(reader, reader.uint32(), options, message.custom);
|
|
2000
|
+
break;
|
|
2001
|
+
case /* google.protobuf.Timestamp created_at */ 6:
|
|
2002
|
+
message.createdAt = Timestamp.internalBinaryRead(reader, reader.uint32(), options, message.createdAt);
|
|
2003
|
+
break;
|
|
2004
|
+
case /* google.protobuf.Timestamp updated_at */ 7:
|
|
2005
|
+
message.updatedAt = Timestamp.internalBinaryRead(reader, reader.uint32(), options, message.updatedAt);
|
|
2006
|
+
break;
|
|
2007
|
+
default:
|
|
2008
|
+
let u = options.readUnknownField;
|
|
2009
|
+
if (u === 'throw')
|
|
2010
|
+
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
|
2011
|
+
let d = reader.skip(wireType);
|
|
2012
|
+
if (u !== false)
|
|
2013
|
+
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
return message;
|
|
2017
|
+
}
|
|
2018
|
+
internalBinaryWrite(message, writer, options) {
|
|
2019
|
+
/* string type = 1; */
|
|
2020
|
+
if (message.type !== '')
|
|
2021
|
+
writer.tag(1, WireType.LengthDelimited).string(message.type);
|
|
2022
|
+
/* string id = 2; */
|
|
2023
|
+
if (message.id !== '')
|
|
2024
|
+
writer.tag(2, WireType.LengthDelimited).string(message.id);
|
|
2025
|
+
/* string created_by_user_id = 3; */
|
|
2026
|
+
if (message.createdByUserId !== '')
|
|
2027
|
+
writer.tag(3, WireType.LengthDelimited).string(message.createdByUserId);
|
|
2028
|
+
/* string host_user_id = 4; */
|
|
2029
|
+
if (message.hostUserId !== '')
|
|
2030
|
+
writer.tag(4, WireType.LengthDelimited).string(message.hostUserId);
|
|
2031
|
+
/* google.protobuf.Struct custom = 5; */
|
|
2032
|
+
if (message.custom)
|
|
2033
|
+
Struct.internalBinaryWrite(message.custom, writer.tag(5, WireType.LengthDelimited).fork(), options).join();
|
|
2034
|
+
/* google.protobuf.Timestamp created_at = 6; */
|
|
2035
|
+
if (message.createdAt)
|
|
2036
|
+
Timestamp.internalBinaryWrite(message.createdAt, writer.tag(6, WireType.LengthDelimited).fork(), options).join();
|
|
2037
|
+
/* google.protobuf.Timestamp updated_at = 7; */
|
|
2038
|
+
if (message.updatedAt)
|
|
2039
|
+
Timestamp.internalBinaryWrite(message.updatedAt, writer.tag(7, WireType.LengthDelimited).fork(), options).join();
|
|
2040
|
+
let u = options.writeUnknownFields;
|
|
2041
|
+
if (u !== false)
|
|
2042
|
+
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
|
2043
|
+
return writer;
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
/**
|
|
2047
|
+
* @generated MessageType for protobuf message stream.video.sfu.models.Call
|
|
2048
|
+
*/
|
|
2049
|
+
const Call$1 = new Call$Type();
|
|
2050
|
+
// @generated message type with reflection information, may provide speed optimized methods
|
|
1954
2051
|
let Error$Type$1 = class Error$Type extends MessageType {
|
|
1955
2052
|
constructor() {
|
|
1956
2053
|
super('stream.video.sfu.models.Error', [
|
|
@@ -2344,108 +2441,6 @@ class Device$Type extends MessageType {
|
|
|
2344
2441
|
*/
|
|
2345
2442
|
const Device = new Device$Type();
|
|
2346
2443
|
// @generated message type with reflection information, may provide speed optimized methods
|
|
2347
|
-
class Call$Type extends MessageType {
|
|
2348
|
-
constructor() {
|
|
2349
|
-
super('stream.video.sfu.models.Call', [
|
|
2350
|
-
{ no: 1, name: 'type', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
|
|
2351
|
-
{ no: 2, name: 'id', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
|
|
2352
|
-
{
|
|
2353
|
-
no: 3,
|
|
2354
|
-
name: 'created_by_user_id',
|
|
2355
|
-
kind: 'scalar',
|
|
2356
|
-
T: 9 /*ScalarType.STRING*/,
|
|
2357
|
-
},
|
|
2358
|
-
{
|
|
2359
|
-
no: 4,
|
|
2360
|
-
name: 'host_user_id',
|
|
2361
|
-
kind: 'scalar',
|
|
2362
|
-
T: 9 /*ScalarType.STRING*/,
|
|
2363
|
-
},
|
|
2364
|
-
{ no: 5, name: 'custom', kind: 'message', T: () => Struct },
|
|
2365
|
-
{ no: 6, name: 'created_at', kind: 'message', T: () => Timestamp },
|
|
2366
|
-
{ no: 7, name: 'updated_at', kind: 'message', T: () => Timestamp },
|
|
2367
|
-
]);
|
|
2368
|
-
}
|
|
2369
|
-
create(value) {
|
|
2370
|
-
const message = globalThis.Object.create(this.messagePrototype);
|
|
2371
|
-
message.type = '';
|
|
2372
|
-
message.id = '';
|
|
2373
|
-
message.createdByUserId = '';
|
|
2374
|
-
message.hostUserId = '';
|
|
2375
|
-
if (value !== undefined)
|
|
2376
|
-
reflectionMergePartial(this, message, value);
|
|
2377
|
-
return message;
|
|
2378
|
-
}
|
|
2379
|
-
internalBinaryRead(reader, length, options, target) {
|
|
2380
|
-
let message = target ?? this.create(), end = reader.pos + length;
|
|
2381
|
-
while (reader.pos < end) {
|
|
2382
|
-
let [fieldNo, wireType] = reader.tag();
|
|
2383
|
-
switch (fieldNo) {
|
|
2384
|
-
case /* string type */ 1:
|
|
2385
|
-
message.type = reader.string();
|
|
2386
|
-
break;
|
|
2387
|
-
case /* string id */ 2:
|
|
2388
|
-
message.id = reader.string();
|
|
2389
|
-
break;
|
|
2390
|
-
case /* string created_by_user_id */ 3:
|
|
2391
|
-
message.createdByUserId = reader.string();
|
|
2392
|
-
break;
|
|
2393
|
-
case /* string host_user_id */ 4:
|
|
2394
|
-
message.hostUserId = reader.string();
|
|
2395
|
-
break;
|
|
2396
|
-
case /* google.protobuf.Struct custom */ 5:
|
|
2397
|
-
message.custom = Struct.internalBinaryRead(reader, reader.uint32(), options, message.custom);
|
|
2398
|
-
break;
|
|
2399
|
-
case /* google.protobuf.Timestamp created_at */ 6:
|
|
2400
|
-
message.createdAt = Timestamp.internalBinaryRead(reader, reader.uint32(), options, message.createdAt);
|
|
2401
|
-
break;
|
|
2402
|
-
case /* google.protobuf.Timestamp updated_at */ 7:
|
|
2403
|
-
message.updatedAt = Timestamp.internalBinaryRead(reader, reader.uint32(), options, message.updatedAt);
|
|
2404
|
-
break;
|
|
2405
|
-
default:
|
|
2406
|
-
let u = options.readUnknownField;
|
|
2407
|
-
if (u === 'throw')
|
|
2408
|
-
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
|
2409
|
-
let d = reader.skip(wireType);
|
|
2410
|
-
if (u !== false)
|
|
2411
|
-
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
|
2412
|
-
}
|
|
2413
|
-
}
|
|
2414
|
-
return message;
|
|
2415
|
-
}
|
|
2416
|
-
internalBinaryWrite(message, writer, options) {
|
|
2417
|
-
/* string type = 1; */
|
|
2418
|
-
if (message.type !== '')
|
|
2419
|
-
writer.tag(1, WireType.LengthDelimited).string(message.type);
|
|
2420
|
-
/* string id = 2; */
|
|
2421
|
-
if (message.id !== '')
|
|
2422
|
-
writer.tag(2, WireType.LengthDelimited).string(message.id);
|
|
2423
|
-
/* string created_by_user_id = 3; */
|
|
2424
|
-
if (message.createdByUserId !== '')
|
|
2425
|
-
writer.tag(3, WireType.LengthDelimited).string(message.createdByUserId);
|
|
2426
|
-
/* string host_user_id = 4; */
|
|
2427
|
-
if (message.hostUserId !== '')
|
|
2428
|
-
writer.tag(4, WireType.LengthDelimited).string(message.hostUserId);
|
|
2429
|
-
/* google.protobuf.Struct custom = 5; */
|
|
2430
|
-
if (message.custom)
|
|
2431
|
-
Struct.internalBinaryWrite(message.custom, writer.tag(5, WireType.LengthDelimited).fork(), options).join();
|
|
2432
|
-
/* google.protobuf.Timestamp created_at = 6; */
|
|
2433
|
-
if (message.createdAt)
|
|
2434
|
-
Timestamp.internalBinaryWrite(message.createdAt, writer.tag(6, WireType.LengthDelimited).fork(), options).join();
|
|
2435
|
-
/* google.protobuf.Timestamp updated_at = 7; */
|
|
2436
|
-
if (message.updatedAt)
|
|
2437
|
-
Timestamp.internalBinaryWrite(message.updatedAt, writer.tag(7, WireType.LengthDelimited).fork(), options).join();
|
|
2438
|
-
let u = options.writeUnknownFields;
|
|
2439
|
-
if (u !== false)
|
|
2440
|
-
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
|
2441
|
-
return writer;
|
|
2442
|
-
}
|
|
2443
|
-
}
|
|
2444
|
-
/**
|
|
2445
|
-
* @generated MessageType for protobuf message stream.video.sfu.models.Call
|
|
2446
|
-
*/
|
|
2447
|
-
const Call$1 = new Call$Type();
|
|
2448
|
-
// @generated message type with reflection information, may provide speed optimized methods
|
|
2449
2444
|
class CallGrants$Type extends MessageType {
|
|
2450
2445
|
constructor() {
|
|
2451
2446
|
super('stream.video.sfu.models.CallGrants', [
|
|
@@ -3976,13 +3971,6 @@ class SfuEvent$Type extends MessageType {
|
|
|
3976
3971
|
oneof: 'eventPayload',
|
|
3977
3972
|
T: () => ParticipantUpdated,
|
|
3978
3973
|
},
|
|
3979
|
-
{
|
|
3980
|
-
no: 25,
|
|
3981
|
-
name: 'participant_migration_complete',
|
|
3982
|
-
kind: 'message',
|
|
3983
|
-
oneof: 'eventPayload',
|
|
3984
|
-
T: () => ParticipantMigrationComplete,
|
|
3985
|
-
},
|
|
3986
3974
|
]);
|
|
3987
3975
|
}
|
|
3988
3976
|
create(value) {
|
|
@@ -4117,12 +4105,6 @@ class SfuEvent$Type extends MessageType {
|
|
|
4117
4105
|
participantUpdated: ParticipantUpdated.internalBinaryRead(reader, reader.uint32(), options, message.eventPayload.participantUpdated),
|
|
4118
4106
|
};
|
|
4119
4107
|
break;
|
|
4120
|
-
case /* stream.video.sfu.event.ParticipantMigrationComplete participant_migration_complete */ 25:
|
|
4121
|
-
message.eventPayload = {
|
|
4122
|
-
oneofKind: 'participantMigrationComplete',
|
|
4123
|
-
participantMigrationComplete: ParticipantMigrationComplete.internalBinaryRead(reader, reader.uint32(), options, message.eventPayload.participantMigrationComplete),
|
|
4124
|
-
};
|
|
4125
|
-
break;
|
|
4126
4108
|
default:
|
|
4127
4109
|
let u = options.readUnknownField;
|
|
4128
4110
|
if (u === 'throw')
|
|
@@ -4195,9 +4177,6 @@ class SfuEvent$Type extends MessageType {
|
|
|
4195
4177
|
/* stream.video.sfu.event.ParticipantUpdated participant_updated = 24; */
|
|
4196
4178
|
if (message.eventPayload.oneofKind === 'participantUpdated')
|
|
4197
4179
|
ParticipantUpdated.internalBinaryWrite(message.eventPayload.participantUpdated, writer.tag(24, WireType.LengthDelimited).fork(), options).join();
|
|
4198
|
-
/* stream.video.sfu.event.ParticipantMigrationComplete participant_migration_complete = 25; */
|
|
4199
|
-
if (message.eventPayload.oneofKind === 'participantMigrationComplete')
|
|
4200
|
-
ParticipantMigrationComplete.internalBinaryWrite(message.eventPayload.participantMigrationComplete, writer.tag(25, WireType.LengthDelimited).fork(), options).join();
|
|
4201
4180
|
let u = options.writeUnknownFields;
|
|
4202
4181
|
if (u !== false)
|
|
4203
4182
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
|
@@ -4209,31 +4188,6 @@ class SfuEvent$Type extends MessageType {
|
|
|
4209
4188
|
*/
|
|
4210
4189
|
const SfuEvent = new SfuEvent$Type();
|
|
4211
4190
|
// @generated message type with reflection information, may provide speed optimized methods
|
|
4212
|
-
class ParticipantMigrationComplete$Type extends MessageType {
|
|
4213
|
-
constructor() {
|
|
4214
|
-
super('stream.video.sfu.event.ParticipantMigrationComplete', []);
|
|
4215
|
-
}
|
|
4216
|
-
create(value) {
|
|
4217
|
-
const message = globalThis.Object.create(this.messagePrototype);
|
|
4218
|
-
if (value !== undefined)
|
|
4219
|
-
reflectionMergePartial(this, message, value);
|
|
4220
|
-
return message;
|
|
4221
|
-
}
|
|
4222
|
-
internalBinaryRead(reader, length, options, target) {
|
|
4223
|
-
return target ?? this.create();
|
|
4224
|
-
}
|
|
4225
|
-
internalBinaryWrite(message, writer, options) {
|
|
4226
|
-
let u = options.writeUnknownFields;
|
|
4227
|
-
if (u !== false)
|
|
4228
|
-
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
|
4229
|
-
return writer;
|
|
4230
|
-
}
|
|
4231
|
-
}
|
|
4232
|
-
/**
|
|
4233
|
-
* @generated MessageType for protobuf message stream.video.sfu.event.ParticipantMigrationComplete
|
|
4234
|
-
*/
|
|
4235
|
-
const ParticipantMigrationComplete = new ParticipantMigrationComplete$Type();
|
|
4236
|
-
// @generated message type with reflection information, may provide speed optimized methods
|
|
4237
4191
|
class PinsChanged$Type extends MessageType {
|
|
4238
4192
|
constructor() {
|
|
4239
4193
|
super('stream.video.sfu.event.PinsChanged', [
|
|
@@ -4484,13 +4438,6 @@ class SfuRequest$Type extends MessageType {
|
|
|
4484
4438
|
oneof: 'requestPayload',
|
|
4485
4439
|
T: () => HealthCheckRequest,
|
|
4486
4440
|
},
|
|
4487
|
-
{
|
|
4488
|
-
no: 3,
|
|
4489
|
-
name: 'leave_call_request',
|
|
4490
|
-
kind: 'message',
|
|
4491
|
-
oneof: 'requestPayload',
|
|
4492
|
-
T: () => LeaveCallRequest,
|
|
4493
|
-
},
|
|
4494
4441
|
]);
|
|
4495
4442
|
}
|
|
4496
4443
|
create(value) {
|
|
@@ -4517,12 +4464,6 @@ class SfuRequest$Type extends MessageType {
|
|
|
4517
4464
|
healthCheckRequest: HealthCheckRequest.internalBinaryRead(reader, reader.uint32(), options, message.requestPayload.healthCheckRequest),
|
|
4518
4465
|
};
|
|
4519
4466
|
break;
|
|
4520
|
-
case /* stream.video.sfu.event.LeaveCallRequest leave_call_request */ 3:
|
|
4521
|
-
message.requestPayload = {
|
|
4522
|
-
oneofKind: 'leaveCallRequest',
|
|
4523
|
-
leaveCallRequest: LeaveCallRequest.internalBinaryRead(reader, reader.uint32(), options, message.requestPayload.leaveCallRequest),
|
|
4524
|
-
};
|
|
4525
|
-
break;
|
|
4526
4467
|
default:
|
|
4527
4468
|
let u = options.readUnknownField;
|
|
4528
4469
|
if (u === 'throw')
|
|
@@ -4541,9 +4482,6 @@ class SfuRequest$Type extends MessageType {
|
|
|
4541
4482
|
/* stream.video.sfu.event.HealthCheckRequest health_check_request = 2; */
|
|
4542
4483
|
if (message.requestPayload.oneofKind === 'healthCheckRequest')
|
|
4543
4484
|
HealthCheckRequest.internalBinaryWrite(message.requestPayload.healthCheckRequest, writer.tag(2, WireType.LengthDelimited).fork(), options).join();
|
|
4544
|
-
/* stream.video.sfu.event.LeaveCallRequest leave_call_request = 3; */
|
|
4545
|
-
if (message.requestPayload.oneofKind === 'leaveCallRequest')
|
|
4546
|
-
LeaveCallRequest.internalBinaryWrite(message.requestPayload.leaveCallRequest, writer.tag(3, WireType.LengthDelimited).fork(), options).join();
|
|
4547
4485
|
let u = options.writeUnknownFields;
|
|
4548
4486
|
if (u !== false)
|
|
4549
4487
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
|
@@ -4555,64 +4493,9 @@ class SfuRequest$Type extends MessageType {
|
|
|
4555
4493
|
*/
|
|
4556
4494
|
const SfuRequest = new SfuRequest$Type();
|
|
4557
4495
|
// @generated message type with reflection information, may provide speed optimized methods
|
|
4558
|
-
class
|
|
4496
|
+
class HealthCheckRequest$Type extends MessageType {
|
|
4559
4497
|
constructor() {
|
|
4560
|
-
super('stream.video.sfu.event.
|
|
4561
|
-
{ no: 1, name: 'session_id', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
|
|
4562
|
-
{ no: 2, name: 'reason', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
|
|
4563
|
-
]);
|
|
4564
|
-
}
|
|
4565
|
-
create(value) {
|
|
4566
|
-
const message = globalThis.Object.create(this.messagePrototype);
|
|
4567
|
-
message.sessionId = '';
|
|
4568
|
-
message.reason = '';
|
|
4569
|
-
if (value !== undefined)
|
|
4570
|
-
reflectionMergePartial(this, message, value);
|
|
4571
|
-
return message;
|
|
4572
|
-
}
|
|
4573
|
-
internalBinaryRead(reader, length, options, target) {
|
|
4574
|
-
let message = target ?? this.create(), end = reader.pos + length;
|
|
4575
|
-
while (reader.pos < end) {
|
|
4576
|
-
let [fieldNo, wireType] = reader.tag();
|
|
4577
|
-
switch (fieldNo) {
|
|
4578
|
-
case /* string session_id */ 1:
|
|
4579
|
-
message.sessionId = reader.string();
|
|
4580
|
-
break;
|
|
4581
|
-
case /* string reason */ 2:
|
|
4582
|
-
message.reason = reader.string();
|
|
4583
|
-
break;
|
|
4584
|
-
default:
|
|
4585
|
-
let u = options.readUnknownField;
|
|
4586
|
-
if (u === 'throw')
|
|
4587
|
-
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
|
4588
|
-
let d = reader.skip(wireType);
|
|
4589
|
-
if (u !== false)
|
|
4590
|
-
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
|
4591
|
-
}
|
|
4592
|
-
}
|
|
4593
|
-
return message;
|
|
4594
|
-
}
|
|
4595
|
-
internalBinaryWrite(message, writer, options) {
|
|
4596
|
-
/* string session_id = 1; */
|
|
4597
|
-
if (message.sessionId !== '')
|
|
4598
|
-
writer.tag(1, WireType.LengthDelimited).string(message.sessionId);
|
|
4599
|
-
/* string reason = 2; */
|
|
4600
|
-
if (message.reason !== '')
|
|
4601
|
-
writer.tag(2, WireType.LengthDelimited).string(message.reason);
|
|
4602
|
-
let u = options.writeUnknownFields;
|
|
4603
|
-
if (u !== false)
|
|
4604
|
-
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
|
4605
|
-
return writer;
|
|
4606
|
-
}
|
|
4607
|
-
}
|
|
4608
|
-
/**
|
|
4609
|
-
* @generated MessageType for protobuf message stream.video.sfu.event.LeaveCallRequest
|
|
4610
|
-
*/
|
|
4611
|
-
const LeaveCallRequest = new LeaveCallRequest$Type();
|
|
4612
|
-
// @generated message type with reflection information, may provide speed optimized methods
|
|
4613
|
-
class HealthCheckRequest$Type extends MessageType {
|
|
4614
|
-
constructor() {
|
|
4615
|
-
super('stream.video.sfu.event.HealthCheckRequest', []);
|
|
4498
|
+
super('stream.video.sfu.event.HealthCheckRequest', []);
|
|
4616
4499
|
}
|
|
4617
4500
|
create(value) {
|
|
4618
4501
|
const message = globalThis.Object.create(this.messagePrototype);
|
|
@@ -4885,12 +4768,6 @@ class JoinRequest$Type extends MessageType {
|
|
|
4885
4768
|
kind: 'scalar',
|
|
4886
4769
|
T: 8 /*ScalarType.BOOL*/,
|
|
4887
4770
|
},
|
|
4888
|
-
{
|
|
4889
|
-
no: 7,
|
|
4890
|
-
name: 'reconnect_details',
|
|
4891
|
-
kind: 'message',
|
|
4892
|
-
T: () => ReconnectDetails,
|
|
4893
|
-
},
|
|
4894
4771
|
]);
|
|
4895
4772
|
}
|
|
4896
4773
|
create(value) {
|
|
@@ -4920,15 +4797,12 @@ class JoinRequest$Type extends MessageType {
|
|
|
4920
4797
|
case /* stream.video.sfu.models.ClientDetails client_details */ 4:
|
|
4921
4798
|
message.clientDetails = ClientDetails.internalBinaryRead(reader, reader.uint32(), options, message.clientDetails);
|
|
4922
4799
|
break;
|
|
4923
|
-
case /* stream.video.sfu.event.Migration migration
|
|
4800
|
+
case /* stream.video.sfu.event.Migration migration */ 5:
|
|
4924
4801
|
message.migration = Migration.internalBinaryRead(reader, reader.uint32(), options, message.migration);
|
|
4925
4802
|
break;
|
|
4926
|
-
case /* bool fast_reconnect
|
|
4803
|
+
case /* bool fast_reconnect */ 6:
|
|
4927
4804
|
message.fastReconnect = reader.bool();
|
|
4928
4805
|
break;
|
|
4929
|
-
case /* stream.video.sfu.event.ReconnectDetails reconnect_details */ 7:
|
|
4930
|
-
message.reconnectDetails = ReconnectDetails.internalBinaryRead(reader, reader.uint32(), options, message.reconnectDetails);
|
|
4931
|
-
break;
|
|
4932
4806
|
default:
|
|
4933
4807
|
let u = options.readUnknownField;
|
|
4934
4808
|
if (u === 'throw')
|
|
@@ -4953,15 +4827,12 @@ class JoinRequest$Type extends MessageType {
|
|
|
4953
4827
|
/* stream.video.sfu.models.ClientDetails client_details = 4; */
|
|
4954
4828
|
if (message.clientDetails)
|
|
4955
4829
|
ClientDetails.internalBinaryWrite(message.clientDetails, writer.tag(4, WireType.LengthDelimited).fork(), options).join();
|
|
4956
|
-
/* stream.video.sfu.event.Migration migration = 5
|
|
4830
|
+
/* stream.video.sfu.event.Migration migration = 5; */
|
|
4957
4831
|
if (message.migration)
|
|
4958
4832
|
Migration.internalBinaryWrite(message.migration, writer.tag(5, WireType.LengthDelimited).fork(), options).join();
|
|
4959
|
-
/* bool fast_reconnect = 6
|
|
4833
|
+
/* bool fast_reconnect = 6; */
|
|
4960
4834
|
if (message.fastReconnect !== false)
|
|
4961
4835
|
writer.tag(6, WireType.Varint).bool(message.fastReconnect);
|
|
4962
|
-
/* stream.video.sfu.event.ReconnectDetails reconnect_details = 7; */
|
|
4963
|
-
if (message.reconnectDetails)
|
|
4964
|
-
ReconnectDetails.internalBinaryWrite(message.reconnectDetails, writer.tag(7, WireType.LengthDelimited).fork(), options).join();
|
|
4965
4836
|
let u = options.writeUnknownFields;
|
|
4966
4837
|
if (u !== false)
|
|
4967
4838
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
|
@@ -4973,129 +4844,6 @@ class JoinRequest$Type extends MessageType {
|
|
|
4973
4844
|
*/
|
|
4974
4845
|
const JoinRequest = new JoinRequest$Type();
|
|
4975
4846
|
// @generated message type with reflection information, may provide speed optimized methods
|
|
4976
|
-
class ReconnectDetails$Type extends MessageType {
|
|
4977
|
-
constructor() {
|
|
4978
|
-
super('stream.video.sfu.event.ReconnectDetails', [
|
|
4979
|
-
{
|
|
4980
|
-
no: 1,
|
|
4981
|
-
name: 'strategy',
|
|
4982
|
-
kind: 'enum',
|
|
4983
|
-
T: () => [
|
|
4984
|
-
'stream.video.sfu.models.WebsocketReconnectStrategy',
|
|
4985
|
-
WebsocketReconnectStrategy,
|
|
4986
|
-
'WEBSOCKET_RECONNECT_STRATEGY_',
|
|
4987
|
-
],
|
|
4988
|
-
},
|
|
4989
|
-
{
|
|
4990
|
-
no: 3,
|
|
4991
|
-
name: 'announced_tracks',
|
|
4992
|
-
kind: 'message',
|
|
4993
|
-
repeat: 1 /*RepeatType.PACKED*/,
|
|
4994
|
-
T: () => TrackInfo,
|
|
4995
|
-
},
|
|
4996
|
-
{
|
|
4997
|
-
no: 4,
|
|
4998
|
-
name: 'subscriptions',
|
|
4999
|
-
kind: 'message',
|
|
5000
|
-
repeat: 1 /*RepeatType.PACKED*/,
|
|
5001
|
-
T: () => TrackSubscriptionDetails,
|
|
5002
|
-
},
|
|
5003
|
-
{
|
|
5004
|
-
no: 5,
|
|
5005
|
-
name: 'reconnect_attempt',
|
|
5006
|
-
kind: 'scalar',
|
|
5007
|
-
T: 13 /*ScalarType.UINT32*/,
|
|
5008
|
-
},
|
|
5009
|
-
{
|
|
5010
|
-
no: 6,
|
|
5011
|
-
name: 'from_sfu_id',
|
|
5012
|
-
kind: 'scalar',
|
|
5013
|
-
T: 9 /*ScalarType.STRING*/,
|
|
5014
|
-
},
|
|
5015
|
-
{
|
|
5016
|
-
no: 7,
|
|
5017
|
-
name: 'previous_session_id',
|
|
5018
|
-
kind: 'scalar',
|
|
5019
|
-
T: 9 /*ScalarType.STRING*/,
|
|
5020
|
-
},
|
|
5021
|
-
]);
|
|
5022
|
-
}
|
|
5023
|
-
create(value) {
|
|
5024
|
-
const message = globalThis.Object.create(this.messagePrototype);
|
|
5025
|
-
message.strategy = 0;
|
|
5026
|
-
message.announcedTracks = [];
|
|
5027
|
-
message.subscriptions = [];
|
|
5028
|
-
message.reconnectAttempt = 0;
|
|
5029
|
-
message.fromSfuId = '';
|
|
5030
|
-
message.previousSessionId = '';
|
|
5031
|
-
if (value !== undefined)
|
|
5032
|
-
reflectionMergePartial(this, message, value);
|
|
5033
|
-
return message;
|
|
5034
|
-
}
|
|
5035
|
-
internalBinaryRead(reader, length, options, target) {
|
|
5036
|
-
let message = target ?? this.create(), end = reader.pos + length;
|
|
5037
|
-
while (reader.pos < end) {
|
|
5038
|
-
let [fieldNo, wireType] = reader.tag();
|
|
5039
|
-
switch (fieldNo) {
|
|
5040
|
-
case /* stream.video.sfu.models.WebsocketReconnectStrategy strategy */ 1:
|
|
5041
|
-
message.strategy = reader.int32();
|
|
5042
|
-
break;
|
|
5043
|
-
case /* repeated stream.video.sfu.models.TrackInfo announced_tracks */ 3:
|
|
5044
|
-
message.announcedTracks.push(TrackInfo.internalBinaryRead(reader, reader.uint32(), options));
|
|
5045
|
-
break;
|
|
5046
|
-
case /* repeated stream.video.sfu.signal.TrackSubscriptionDetails subscriptions */ 4:
|
|
5047
|
-
message.subscriptions.push(TrackSubscriptionDetails.internalBinaryRead(reader, reader.uint32(), options));
|
|
5048
|
-
break;
|
|
5049
|
-
case /* uint32 reconnect_attempt */ 5:
|
|
5050
|
-
message.reconnectAttempt = reader.uint32();
|
|
5051
|
-
break;
|
|
5052
|
-
case /* string from_sfu_id */ 6:
|
|
5053
|
-
message.fromSfuId = reader.string();
|
|
5054
|
-
break;
|
|
5055
|
-
case /* string previous_session_id */ 7:
|
|
5056
|
-
message.previousSessionId = reader.string();
|
|
5057
|
-
break;
|
|
5058
|
-
default:
|
|
5059
|
-
let u = options.readUnknownField;
|
|
5060
|
-
if (u === 'throw')
|
|
5061
|
-
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
|
5062
|
-
let d = reader.skip(wireType);
|
|
5063
|
-
if (u !== false)
|
|
5064
|
-
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
|
5065
|
-
}
|
|
5066
|
-
}
|
|
5067
|
-
return message;
|
|
5068
|
-
}
|
|
5069
|
-
internalBinaryWrite(message, writer, options) {
|
|
5070
|
-
/* stream.video.sfu.models.WebsocketReconnectStrategy strategy = 1; */
|
|
5071
|
-
if (message.strategy !== 0)
|
|
5072
|
-
writer.tag(1, WireType.Varint).int32(message.strategy);
|
|
5073
|
-
/* repeated stream.video.sfu.models.TrackInfo announced_tracks = 3; */
|
|
5074
|
-
for (let i = 0; i < message.announcedTracks.length; i++)
|
|
5075
|
-
TrackInfo.internalBinaryWrite(message.announcedTracks[i], writer.tag(3, WireType.LengthDelimited).fork(), options).join();
|
|
5076
|
-
/* repeated stream.video.sfu.signal.TrackSubscriptionDetails subscriptions = 4; */
|
|
5077
|
-
for (let i = 0; i < message.subscriptions.length; i++)
|
|
5078
|
-
TrackSubscriptionDetails.internalBinaryWrite(message.subscriptions[i], writer.tag(4, WireType.LengthDelimited).fork(), options).join();
|
|
5079
|
-
/* uint32 reconnect_attempt = 5; */
|
|
5080
|
-
if (message.reconnectAttempt !== 0)
|
|
5081
|
-
writer.tag(5, WireType.Varint).uint32(message.reconnectAttempt);
|
|
5082
|
-
/* string from_sfu_id = 6; */
|
|
5083
|
-
if (message.fromSfuId !== '')
|
|
5084
|
-
writer.tag(6, WireType.LengthDelimited).string(message.fromSfuId);
|
|
5085
|
-
/* string previous_session_id = 7; */
|
|
5086
|
-
if (message.previousSessionId !== '')
|
|
5087
|
-
writer.tag(7, WireType.LengthDelimited).string(message.previousSessionId);
|
|
5088
|
-
let u = options.writeUnknownFields;
|
|
5089
|
-
if (u !== false)
|
|
5090
|
-
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
|
5091
|
-
return writer;
|
|
5092
|
-
}
|
|
5093
|
-
}
|
|
5094
|
-
/**
|
|
5095
|
-
* @generated MessageType for protobuf message stream.video.sfu.event.ReconnectDetails
|
|
5096
|
-
*/
|
|
5097
|
-
const ReconnectDetails = new ReconnectDetails$Type();
|
|
5098
|
-
// @generated message type with reflection information, may provide speed optimized methods
|
|
5099
4847
|
class Migration$Type extends MessageType {
|
|
5100
4848
|
constructor() {
|
|
5101
4849
|
super('stream.video.sfu.event.Migration', [
|
|
@@ -5181,18 +4929,11 @@ class JoinResponse$Type extends MessageType {
|
|
|
5181
4929
|
super('stream.video.sfu.event.JoinResponse', [
|
|
5182
4930
|
{ no: 1, name: 'call_state', kind: 'message', T: () => CallState$1 },
|
|
5183
4931
|
{ no: 2, name: 'reconnected', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
|
|
5184
|
-
{
|
|
5185
|
-
no: 3,
|
|
5186
|
-
name: 'fast_reconnect_deadline_seconds',
|
|
5187
|
-
kind: 'scalar',
|
|
5188
|
-
T: 5 /*ScalarType.INT32*/,
|
|
5189
|
-
},
|
|
5190
4932
|
]);
|
|
5191
4933
|
}
|
|
5192
4934
|
create(value) {
|
|
5193
4935
|
const message = globalThis.Object.create(this.messagePrototype);
|
|
5194
4936
|
message.reconnected = false;
|
|
5195
|
-
message.fastReconnectDeadlineSeconds = 0;
|
|
5196
4937
|
if (value !== undefined)
|
|
5197
4938
|
reflectionMergePartial(this, message, value);
|
|
5198
4939
|
return message;
|
|
@@ -5208,9 +4949,6 @@ class JoinResponse$Type extends MessageType {
|
|
|
5208
4949
|
case /* bool reconnected */ 2:
|
|
5209
4950
|
message.reconnected = reader.bool();
|
|
5210
4951
|
break;
|
|
5211
|
-
case /* int32 fast_reconnect_deadline_seconds */ 3:
|
|
5212
|
-
message.fastReconnectDeadlineSeconds = reader.int32();
|
|
5213
|
-
break;
|
|
5214
4952
|
default:
|
|
5215
4953
|
let u = options.readUnknownField;
|
|
5216
4954
|
if (u === 'throw')
|
|
@@ -5229,11 +4967,6 @@ class JoinResponse$Type extends MessageType {
|
|
|
5229
4967
|
/* bool reconnected = 2; */
|
|
5230
4968
|
if (message.reconnected !== false)
|
|
5231
4969
|
writer.tag(2, WireType.Varint).bool(message.reconnected);
|
|
5232
|
-
/* int32 fast_reconnect_deadline_seconds = 3; */
|
|
5233
|
-
if (message.fastReconnectDeadlineSeconds !== 0)
|
|
5234
|
-
writer
|
|
5235
|
-
.tag(3, WireType.Varint)
|
|
5236
|
-
.int32(message.fastReconnectDeadlineSeconds);
|
|
5237
4970
|
let u = options.writeUnknownFields;
|
|
5238
4971
|
if (u !== false)
|
|
5239
4972
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
|
@@ -6434,15 +6167,12 @@ var events = /*#__PURE__*/Object.freeze({
|
|
|
6434
6167
|
ICETrickle: ICETrickle,
|
|
6435
6168
|
JoinRequest: JoinRequest,
|
|
6436
6169
|
JoinResponse: JoinResponse,
|
|
6437
|
-
LeaveCallRequest: LeaveCallRequest,
|
|
6438
6170
|
Migration: Migration,
|
|
6439
6171
|
ParticipantJoined: ParticipantJoined,
|
|
6440
6172
|
ParticipantLeft: ParticipantLeft,
|
|
6441
|
-
ParticipantMigrationComplete: ParticipantMigrationComplete,
|
|
6442
6173
|
ParticipantUpdated: ParticipantUpdated,
|
|
6443
6174
|
PinsChanged: PinsChanged,
|
|
6444
6175
|
PublisherAnswer: PublisherAnswer,
|
|
6445
|
-
ReconnectDetails: ReconnectDetails,
|
|
6446
6176
|
SfuEvent: SfuEvent,
|
|
6447
6177
|
SfuRequest: SfuRequest,
|
|
6448
6178
|
SubscriberOffer: SubscriberOffer,
|
|
@@ -6568,17 +6298,6 @@ const withHeaders = (headers) => {
|
|
|
6568
6298
|
},
|
|
6569
6299
|
};
|
|
6570
6300
|
};
|
|
6571
|
-
const withRequestLogger = (logger, level) => {
|
|
6572
|
-
return {
|
|
6573
|
-
interceptUnary: (next, method, input, options) => {
|
|
6574
|
-
logger(level, `Calling SFU RPC method ${method.name}`, {
|
|
6575
|
-
input,
|
|
6576
|
-
options,
|
|
6577
|
-
});
|
|
6578
|
-
return next(method, input, options);
|
|
6579
|
-
},
|
|
6580
|
-
};
|
|
6581
|
-
};
|
|
6582
6301
|
/**
|
|
6583
6302
|
* Creates new SignalServerClient instance.
|
|
6584
6303
|
*
|
|
@@ -6592,196 +6311,68 @@ const createSignalClient = (options) => {
|
|
|
6592
6311
|
return new SignalServerClient(transport);
|
|
6593
6312
|
};
|
|
6594
6313
|
|
|
6595
|
-
const sleep = (m) => new Promise((r) => setTimeout(r, m));
|
|
6596
|
-
function isFunction(value) {
|
|
6597
|
-
return (value &&
|
|
6598
|
-
(Object.prototype.toString.call(value) === '[object Function]' ||
|
|
6599
|
-
'function' === typeof value ||
|
|
6600
|
-
value instanceof Function));
|
|
6601
|
-
}
|
|
6602
6314
|
/**
|
|
6603
|
-
*
|
|
6315
|
+
* Checks whether we are using React Native
|
|
6604
6316
|
*/
|
|
6605
|
-
const
|
|
6606
|
-
|
|
6607
|
-
|
|
6608
|
-
|
|
6609
|
-
WS_POLICY_VIOLATION: 1008,
|
|
6317
|
+
const isReactNative = () => {
|
|
6318
|
+
if (typeof navigator === 'undefined')
|
|
6319
|
+
return false;
|
|
6320
|
+
return navigator.product?.toLowerCase() === 'reactnative';
|
|
6610
6321
|
};
|
|
6611
|
-
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
6622
|
-
|
|
6623
|
-
|
|
6624
|
-
|
|
6625
|
-
|
|
6626
|
-
|
|
6627
|
-
|
|
6628
|
-
|
|
6629
|
-
|
|
6630
|
-
|
|
6631
|
-
|
|
6632
|
-
|
|
6633
|
-
|
|
6634
|
-
|
|
6635
|
-
|
|
6636
|
-
|
|
6637
|
-
|
|
6638
|
-
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
|
|
6643
|
-
|
|
6644
|
-
'
|
|
6645
|
-
|
|
6646
|
-
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
bytes[i] = Math.random() * max;
|
|
6322
|
+
|
|
6323
|
+
// log levels, sorted by verbosity
|
|
6324
|
+
const logLevels = Object.freeze({
|
|
6325
|
+
trace: 0,
|
|
6326
|
+
debug: 1,
|
|
6327
|
+
info: 2,
|
|
6328
|
+
warn: 3,
|
|
6329
|
+
error: 4,
|
|
6330
|
+
});
|
|
6331
|
+
let logger$4;
|
|
6332
|
+
let level = 'info';
|
|
6333
|
+
const logToConsole = (logLevel, message, ...args) => {
|
|
6334
|
+
let logMethod;
|
|
6335
|
+
switch (logLevel) {
|
|
6336
|
+
case 'error':
|
|
6337
|
+
if (isReactNative()) {
|
|
6338
|
+
message = `ERROR: ${message}`;
|
|
6339
|
+
logMethod = console.info;
|
|
6340
|
+
break;
|
|
6341
|
+
}
|
|
6342
|
+
logMethod = console.error;
|
|
6343
|
+
break;
|
|
6344
|
+
case 'warn':
|
|
6345
|
+
if (isReactNative()) {
|
|
6346
|
+
message = `WARN: ${message}`;
|
|
6347
|
+
logMethod = console.info;
|
|
6348
|
+
break;
|
|
6349
|
+
}
|
|
6350
|
+
logMethod = console.warn;
|
|
6351
|
+
break;
|
|
6352
|
+
case 'info':
|
|
6353
|
+
logMethod = console.info;
|
|
6354
|
+
break;
|
|
6355
|
+
case 'trace':
|
|
6356
|
+
logMethod = console.trace;
|
|
6357
|
+
break;
|
|
6358
|
+
default:
|
|
6359
|
+
logMethod = console.log;
|
|
6360
|
+
break;
|
|
6651
6361
|
}
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
else if (typeof msCrypto !== 'undefined') {
|
|
6659
|
-
return msCrypto.getRandomValues.bind(msCrypto);
|
|
6660
|
-
}
|
|
6661
|
-
else {
|
|
6662
|
-
return getRandomValuesWithMathRandom;
|
|
6663
|
-
}
|
|
6664
|
-
})();
|
|
6665
|
-
function getRandomBytes(length) {
|
|
6666
|
-
const bytes = new Uint8Array(length);
|
|
6667
|
-
getRandomValues(bytes);
|
|
6668
|
-
return bytes;
|
|
6669
|
-
}
|
|
6670
|
-
function convertErrorToJson(err) {
|
|
6671
|
-
const jsonObj = {};
|
|
6672
|
-
if (!err)
|
|
6673
|
-
return jsonObj;
|
|
6674
|
-
try {
|
|
6675
|
-
Object.getOwnPropertyNames(err).forEach((key) => {
|
|
6676
|
-
jsonObj[key] = Object.getOwnPropertyDescriptor(err, key);
|
|
6677
|
-
});
|
|
6678
|
-
}
|
|
6679
|
-
catch (_) {
|
|
6680
|
-
return {
|
|
6681
|
-
error: 'failed to serialize the error',
|
|
6682
|
-
};
|
|
6683
|
-
}
|
|
6684
|
-
return jsonObj;
|
|
6685
|
-
}
|
|
6686
|
-
/**
|
|
6687
|
-
* isOnline safely return the navigator.online value for browser env
|
|
6688
|
-
* if navigator is not in global object, it always return true
|
|
6689
|
-
*/
|
|
6690
|
-
function isOnline(logger) {
|
|
6691
|
-
const nav = typeof navigator !== 'undefined'
|
|
6692
|
-
? navigator
|
|
6693
|
-
: typeof window !== 'undefined' && window.navigator
|
|
6694
|
-
? window.navigator
|
|
6695
|
-
: undefined;
|
|
6696
|
-
if (!nav) {
|
|
6697
|
-
logger('warn', 'isOnline failed to access window.navigator and assume browser is online');
|
|
6698
|
-
return true;
|
|
6699
|
-
}
|
|
6700
|
-
// RN navigator has undefined for onLine
|
|
6701
|
-
if (typeof nav.onLine !== 'boolean') {
|
|
6702
|
-
return true;
|
|
6703
|
-
}
|
|
6704
|
-
return nav.onLine;
|
|
6705
|
-
}
|
|
6706
|
-
/**
|
|
6707
|
-
* listenForConnectionChanges - Adds an event listener fired on browser going online or offline
|
|
6708
|
-
*/
|
|
6709
|
-
function addConnectionEventListeners(cb) {
|
|
6710
|
-
if (typeof window !== 'undefined' && window.addEventListener) {
|
|
6711
|
-
window.addEventListener('offline', cb);
|
|
6712
|
-
window.addEventListener('online', cb);
|
|
6713
|
-
}
|
|
6714
|
-
}
|
|
6715
|
-
function removeConnectionEventListeners(cb) {
|
|
6716
|
-
if (typeof window !== 'undefined' && window.removeEventListener) {
|
|
6717
|
-
window.removeEventListener('offline', cb);
|
|
6718
|
-
window.removeEventListener('online', cb);
|
|
6719
|
-
}
|
|
6720
|
-
}
|
|
6721
|
-
|
|
6722
|
-
/**
|
|
6723
|
-
* Checks whether we are using React Native
|
|
6724
|
-
*/
|
|
6725
|
-
const isReactNative = () => {
|
|
6726
|
-
if (typeof navigator === 'undefined')
|
|
6727
|
-
return false;
|
|
6728
|
-
return navigator.product?.toLowerCase() === 'reactnative';
|
|
6729
|
-
};
|
|
6730
|
-
|
|
6731
|
-
// log levels, sorted by verbosity
|
|
6732
|
-
const logLevels = Object.freeze({
|
|
6733
|
-
trace: 0,
|
|
6734
|
-
debug: 1,
|
|
6735
|
-
info: 2,
|
|
6736
|
-
warn: 3,
|
|
6737
|
-
error: 4,
|
|
6738
|
-
});
|
|
6739
|
-
let logger$2;
|
|
6740
|
-
let level = 'info';
|
|
6741
|
-
const logToConsole = (logLevel, message, ...args) => {
|
|
6742
|
-
let logMethod;
|
|
6743
|
-
switch (logLevel) {
|
|
6744
|
-
case 'error':
|
|
6745
|
-
if (isReactNative()) {
|
|
6746
|
-
message = `ERROR: ${message}`;
|
|
6747
|
-
logMethod = console.info;
|
|
6748
|
-
break;
|
|
6749
|
-
}
|
|
6750
|
-
logMethod = console.error;
|
|
6751
|
-
break;
|
|
6752
|
-
case 'warn':
|
|
6753
|
-
if (isReactNative()) {
|
|
6754
|
-
message = `WARN: ${message}`;
|
|
6755
|
-
logMethod = console.info;
|
|
6756
|
-
break;
|
|
6757
|
-
}
|
|
6758
|
-
logMethod = console.warn;
|
|
6759
|
-
break;
|
|
6760
|
-
case 'info':
|
|
6761
|
-
logMethod = console.info;
|
|
6762
|
-
break;
|
|
6763
|
-
case 'trace':
|
|
6764
|
-
logMethod = console.trace;
|
|
6765
|
-
break;
|
|
6766
|
-
default:
|
|
6767
|
-
logMethod = console.log;
|
|
6768
|
-
break;
|
|
6769
|
-
}
|
|
6770
|
-
logMethod(message, ...args);
|
|
6771
|
-
};
|
|
6772
|
-
const setLogger = (l, lvl) => {
|
|
6773
|
-
logger$2 = l;
|
|
6774
|
-
if (lvl) {
|
|
6775
|
-
setLogLevel(lvl);
|
|
6362
|
+
logMethod(message, ...args);
|
|
6363
|
+
};
|
|
6364
|
+
const setLogger = (l, lvl) => {
|
|
6365
|
+
logger$4 = l;
|
|
6366
|
+
if (lvl) {
|
|
6367
|
+
setLogLevel(lvl);
|
|
6776
6368
|
}
|
|
6777
6369
|
};
|
|
6778
6370
|
const setLogLevel = (l) => {
|
|
6779
6371
|
level = l;
|
|
6780
6372
|
};
|
|
6781
|
-
const getLogLevel = () => level;
|
|
6782
6373
|
const getLogger = (withTags) => {
|
|
6783
|
-
const loggerMethod = logger$
|
|
6784
|
-
const tags = (withTags || []).
|
|
6374
|
+
const loggerMethod = logger$4 || logToConsole;
|
|
6375
|
+
const tags = (withTags || []).join(':');
|
|
6785
6376
|
const result = (logLevel, message, ...args) => {
|
|
6786
6377
|
if (logLevels[logLevel] >= logLevels[level]) {
|
|
6787
6378
|
loggerMethod(logLevel, `[${tags}]: ${message}`, ...args);
|
|
@@ -6790,37 +6381,6 @@ const getLogger = (withTags) => {
|
|
|
6790
6381
|
return result;
|
|
6791
6382
|
};
|
|
6792
6383
|
|
|
6793
|
-
/**
|
|
6794
|
-
* Creates a closure which wraps the given RPC call and retries invoking
|
|
6795
|
-
* the RPC until it succeeds or the maximum number of retries is reached.
|
|
6796
|
-
*
|
|
6797
|
-
* For each retry, there would be a delay to avoid request bursts toward the SFU.
|
|
6798
|
-
*
|
|
6799
|
-
* @param rpc the closure around the RPC call to execute.
|
|
6800
|
-
* @param signal the signal to abort the RPC call and retries loop.
|
|
6801
|
-
*/
|
|
6802
|
-
const retryable = async (rpc, signal) => {
|
|
6803
|
-
let attempt = 0;
|
|
6804
|
-
let result = undefined;
|
|
6805
|
-
do {
|
|
6806
|
-
if (attempt > 0)
|
|
6807
|
-
await sleep(retryInterval(attempt));
|
|
6808
|
-
try {
|
|
6809
|
-
result = await rpc();
|
|
6810
|
-
}
|
|
6811
|
-
catch (err) {
|
|
6812
|
-
const isRequestCancelled = err instanceof RpcError &&
|
|
6813
|
-
err.code === TwirpErrorCode[TwirpErrorCode.cancelled];
|
|
6814
|
-
const isAborted = signal?.aborted ?? false;
|
|
6815
|
-
if (isRequestCancelled || isAborted)
|
|
6816
|
-
throw err;
|
|
6817
|
-
getLogger(['sfu-client', 'rpc'])('debug', `rpc failed (${attempt})`, err);
|
|
6818
|
-
attempt++;
|
|
6819
|
-
}
|
|
6820
|
-
} while (!result || result.response.error?.shouldRetry);
|
|
6821
|
-
return result;
|
|
6822
|
-
};
|
|
6823
|
-
|
|
6824
6384
|
const getPreferredCodecs = (kind, preferredCodec, codecToRemove) => {
|
|
6825
6385
|
const logger = getLogger(['codecs']);
|
|
6826
6386
|
if (!('getCapabilities' in RTCRtpReceiver)) {
|
|
@@ -6893,7 +6453,6 @@ const sfuEventKinds = {
|
|
|
6893
6453
|
pinsUpdated: undefined,
|
|
6894
6454
|
callEnded: undefined,
|
|
6895
6455
|
participantUpdated: undefined,
|
|
6896
|
-
participantMigrationComplete: undefined,
|
|
6897
6456
|
};
|
|
6898
6457
|
const isSfuEvent = (eventName) => {
|
|
6899
6458
|
return Object.prototype.hasOwnProperty.call(sfuEventKinds, eventName);
|
|
@@ -6902,12 +6461,12 @@ class Dispatcher {
|
|
|
6902
6461
|
constructor() {
|
|
6903
6462
|
this.logger = getLogger(['Dispatcher']);
|
|
6904
6463
|
this.subscribers = {};
|
|
6905
|
-
this.dispatch = (message
|
|
6464
|
+
this.dispatch = (message) => {
|
|
6906
6465
|
const eventKind = message.eventPayload.oneofKind;
|
|
6907
6466
|
if (!eventKind)
|
|
6908
6467
|
return;
|
|
6909
6468
|
const payload = message.eventPayload[eventKind];
|
|
6910
|
-
this.logger('debug', `Dispatching ${eventKind}
|
|
6469
|
+
this.logger('debug', `Dispatching ${eventKind}`, payload);
|
|
6911
6470
|
const listeners = this.subscribers[eventKind];
|
|
6912
6471
|
if (!listeners)
|
|
6913
6472
|
return;
|
|
@@ -6949,6 +6508,7 @@ class IceTrickleBuffer {
|
|
|
6949
6508
|
constructor() {
|
|
6950
6509
|
this.subscriberCandidates = new ReplaySubject();
|
|
6951
6510
|
this.publisherCandidates = new ReplaySubject();
|
|
6511
|
+
this.logger = getLogger(['sfu-client']);
|
|
6952
6512
|
this.push = (iceTrickle) => {
|
|
6953
6513
|
if (iceTrickle.peerType === PeerType.SUBSCRIBER) {
|
|
6954
6514
|
this.subscriberCandidates.next(iceTrickle);
|
|
@@ -6957,8 +6517,7 @@ class IceTrickleBuffer {
|
|
|
6957
6517
|
this.publisherCandidates.next(iceTrickle);
|
|
6958
6518
|
}
|
|
6959
6519
|
else {
|
|
6960
|
-
|
|
6961
|
-
logger('warn', `ICETrickle, Unknown peer type`, iceTrickle);
|
|
6520
|
+
this.logger('warn', `ICETrickle, Unknown peer type`, iceTrickle);
|
|
6962
6521
|
}
|
|
6963
6522
|
};
|
|
6964
6523
|
}
|
|
@@ -6977,6 +6536,72 @@ function getIceCandidate(candidate) {
|
|
|
6977
6536
|
}
|
|
6978
6537
|
}
|
|
6979
6538
|
|
|
6539
|
+
const version = "1.5.0" ;
|
|
6540
|
+
const [major, minor, patch] = version.split('.');
|
|
6541
|
+
let sdkInfo = {
|
|
6542
|
+
type: SdkType.PLAIN_JAVASCRIPT,
|
|
6543
|
+
major,
|
|
6544
|
+
minor,
|
|
6545
|
+
patch,
|
|
6546
|
+
};
|
|
6547
|
+
let osInfo;
|
|
6548
|
+
let deviceInfo;
|
|
6549
|
+
let webRtcInfo;
|
|
6550
|
+
const setSdkInfo = (info) => {
|
|
6551
|
+
sdkInfo = info;
|
|
6552
|
+
};
|
|
6553
|
+
const getSdkInfo = () => {
|
|
6554
|
+
return sdkInfo;
|
|
6555
|
+
};
|
|
6556
|
+
const setOSInfo = (info) => {
|
|
6557
|
+
osInfo = info;
|
|
6558
|
+
};
|
|
6559
|
+
const getOSInfo = () => {
|
|
6560
|
+
return osInfo;
|
|
6561
|
+
};
|
|
6562
|
+
const setDeviceInfo = (info) => {
|
|
6563
|
+
deviceInfo = info;
|
|
6564
|
+
};
|
|
6565
|
+
const getDeviceInfo = () => {
|
|
6566
|
+
return deviceInfo;
|
|
6567
|
+
};
|
|
6568
|
+
const getWebRTCInfo = () => {
|
|
6569
|
+
return webRtcInfo;
|
|
6570
|
+
};
|
|
6571
|
+
const setWebRTCInfo = (info) => {
|
|
6572
|
+
webRtcInfo = info;
|
|
6573
|
+
};
|
|
6574
|
+
const getClientDetails = () => {
|
|
6575
|
+
if (isReactNative()) {
|
|
6576
|
+
// Since RN doesn't support web, sharing browser info is not required
|
|
6577
|
+
return {
|
|
6578
|
+
sdk: getSdkInfo(),
|
|
6579
|
+
os: getOSInfo(),
|
|
6580
|
+
device: getDeviceInfo(),
|
|
6581
|
+
};
|
|
6582
|
+
}
|
|
6583
|
+
const userAgent = new UAParser(navigator.userAgent);
|
|
6584
|
+
const { browser, os, device, cpu } = userAgent.getResult();
|
|
6585
|
+
return {
|
|
6586
|
+
sdk: getSdkInfo(),
|
|
6587
|
+
browser: {
|
|
6588
|
+
name: browser.name || navigator.userAgent,
|
|
6589
|
+
version: browser.version || '',
|
|
6590
|
+
},
|
|
6591
|
+
os: {
|
|
6592
|
+
name: os.name || '',
|
|
6593
|
+
version: os.version || '',
|
|
6594
|
+
architecture: cpu.architecture || '',
|
|
6595
|
+
},
|
|
6596
|
+
device: {
|
|
6597
|
+
name: [device.vendor, device.model, device.type]
|
|
6598
|
+
.filter(Boolean)
|
|
6599
|
+
.join(' '),
|
|
6600
|
+
version: '',
|
|
6601
|
+
},
|
|
6602
|
+
};
|
|
6603
|
+
};
|
|
6604
|
+
|
|
6980
6605
|
const DEFAULT_BITRATE = 1250000;
|
|
6981
6606
|
const defaultTargetResolution = {
|
|
6982
6607
|
bitrate: DEFAULT_BITRATE,
|
|
@@ -6999,6 +6624,7 @@ const findOptimalVideoLayers = (videoTrack, targetResolution = defaultTargetReso
|
|
|
6999
6624
|
const optimalVideoLayers = [];
|
|
7000
6625
|
const settings = videoTrack.getSettings();
|
|
7001
6626
|
const { width: w = 0, height: h = 0 } = settings;
|
|
6627
|
+
const isRNIos = isReactNative() && getOSInfo()?.name.toLowerCase() === 'ios';
|
|
7002
6628
|
const maxBitrate = getComputedMaxBitrate(targetResolution, w, h);
|
|
7003
6629
|
let downscaleFactor = 1;
|
|
7004
6630
|
['f', 'h', 'q'].forEach((rid) => {
|
|
@@ -7012,7 +6638,12 @@ const findOptimalVideoLayers = (videoTrack, targetResolution = defaultTargetReso
|
|
|
7012
6638
|
height: Math.round(h / downscaleFactor),
|
|
7013
6639
|
maxBitrate: Math.round(maxBitrate / downscaleFactor) || defaultBitratePerRid[rid],
|
|
7014
6640
|
scaleResolutionDownBy: downscaleFactor,
|
|
7015
|
-
|
|
6641
|
+
// Simulcast on iOS React-Native requires all encodings to share the same framerate
|
|
6642
|
+
maxFramerate: {
|
|
6643
|
+
f: 30,
|
|
6644
|
+
h: isRNIos ? 30 : 25,
|
|
6645
|
+
q: isRNIos ? 30 : 20,
|
|
6646
|
+
}[rid],
|
|
7016
6647
|
});
|
|
7017
6648
|
downscaleFactor *= 2;
|
|
7018
6649
|
});
|
|
@@ -7087,10 +6718,6 @@ const findOptimalScreenSharingLayers = (videoTrack, preferences, defaultMaxBitra
|
|
|
7087
6718
|
];
|
|
7088
6719
|
};
|
|
7089
6720
|
|
|
7090
|
-
const ensureExhausted = (x, message) => {
|
|
7091
|
-
getLogger(['helpers'])('warn', message, x);
|
|
7092
|
-
};
|
|
7093
|
-
|
|
7094
6721
|
const trackTypeToParticipantStreamKey = (trackType) => {
|
|
7095
6722
|
switch (trackType) {
|
|
7096
6723
|
case TrackType.SCREEN_SHARE:
|
|
@@ -7104,7 +6731,8 @@ const trackTypeToParticipantStreamKey = (trackType) => {
|
|
|
7104
6731
|
case TrackType.UNSPECIFIED:
|
|
7105
6732
|
throw new Error('Track type is unspecified');
|
|
7106
6733
|
default:
|
|
7107
|
-
|
|
6734
|
+
const exhaustiveTrackTypeCheck = trackType;
|
|
6735
|
+
throw new Error(`Unknown track type: ${exhaustiveTrackTypeCheck}`);
|
|
7108
6736
|
}
|
|
7109
6737
|
};
|
|
7110
6738
|
const muteTypeToTrackType = (muteType) => {
|
|
@@ -7118,21 +6746,8 @@ const muteTypeToTrackType = (muteType) => {
|
|
|
7118
6746
|
case 'screenshare_audio':
|
|
7119
6747
|
return TrackType.SCREEN_SHARE_AUDIO;
|
|
7120
6748
|
default:
|
|
7121
|
-
|
|
7122
|
-
|
|
7123
|
-
};
|
|
7124
|
-
const toTrackType = (trackType) => {
|
|
7125
|
-
switch (trackType) {
|
|
7126
|
-
case 'TRACK_TYPE_AUDIO':
|
|
7127
|
-
return TrackType.AUDIO;
|
|
7128
|
-
case 'TRACK_TYPE_VIDEO':
|
|
7129
|
-
return TrackType.VIDEO;
|
|
7130
|
-
case 'TRACK_TYPE_SCREEN_SHARE':
|
|
7131
|
-
return TrackType.SCREEN_SHARE;
|
|
7132
|
-
case 'TRACK_TYPE_SCREEN_SHARE_AUDIO':
|
|
7133
|
-
return TrackType.SCREEN_SHARE_AUDIO;
|
|
7134
|
-
default:
|
|
7135
|
-
return undefined;
|
|
6749
|
+
const exhaustiveMuteTypeCheck = muteType;
|
|
6750
|
+
throw new Error(`Unknown mute type: ${exhaustiveMuteTypeCheck}`);
|
|
7136
6751
|
}
|
|
7137
6752
|
};
|
|
7138
6753
|
|
|
@@ -7652,11 +7267,6 @@ class CallState {
|
|
|
7652
7267
|
this.anonymousParticipantCountSubject = new BehaviorSubject(0);
|
|
7653
7268
|
this.participantsSubject = new BehaviorSubject([]);
|
|
7654
7269
|
this.callStatsReportSubject = new BehaviorSubject(undefined);
|
|
7655
|
-
// These are tracks that were delivered to the Subscriber's onTrack event
|
|
7656
|
-
// that we couldn't associate with a participant yet.
|
|
7657
|
-
// This happens when the participantJoined event hasn't been received yet.
|
|
7658
|
-
// We keep these tracks around until we can associate them with a participant.
|
|
7659
|
-
this.orphanedTracks = [];
|
|
7660
7270
|
this.logger = getLogger(['CallState']);
|
|
7661
7271
|
/**
|
|
7662
7272
|
* A list of comparators that are used to sort the participants.
|
|
@@ -7846,7 +7456,7 @@ class CallState {
|
|
|
7846
7456
|
*/
|
|
7847
7457
|
this.updateParticipants = (patch) => {
|
|
7848
7458
|
if (Object.keys(patch).length === 0)
|
|
7849
|
-
return
|
|
7459
|
+
return;
|
|
7850
7460
|
return this.setParticipants((participants) => participants.map((p) => {
|
|
7851
7461
|
const thePatch = patch[p.sessionId];
|
|
7852
7462
|
if (thePatch) {
|
|
@@ -7905,41 +7515,6 @@ class CallState {
|
|
|
7905
7515
|
return participant;
|
|
7906
7516
|
}));
|
|
7907
7517
|
};
|
|
7908
|
-
/**
|
|
7909
|
-
* Adds an orphaned track to the call state.
|
|
7910
|
-
*
|
|
7911
|
-
* @internal
|
|
7912
|
-
*
|
|
7913
|
-
* @param orphanedTrack the orphaned track to add.
|
|
7914
|
-
*/
|
|
7915
|
-
this.registerOrphanedTrack = (orphanedTrack) => {
|
|
7916
|
-
this.orphanedTracks.push(orphanedTrack);
|
|
7917
|
-
};
|
|
7918
|
-
/**
|
|
7919
|
-
* Removes an orphaned track from the call state.
|
|
7920
|
-
*
|
|
7921
|
-
* @internal
|
|
7922
|
-
*
|
|
7923
|
-
* @param id the ID of the orphaned track to remove.
|
|
7924
|
-
*/
|
|
7925
|
-
this.removeOrphanedTrack = (id) => {
|
|
7926
|
-
this.orphanedTracks = this.orphanedTracks.filter((o) => o.id !== id);
|
|
7927
|
-
};
|
|
7928
|
-
/**
|
|
7929
|
-
* Takes all orphaned tracks with the given track lookup prefix.
|
|
7930
|
-
* All orphaned tracks with the given track lookup prefix are removed from the call state.
|
|
7931
|
-
*
|
|
7932
|
-
* @internal
|
|
7933
|
-
*
|
|
7934
|
-
* @param trackLookupPrefix the track lookup prefix to match the orphaned tracks by.
|
|
7935
|
-
*/
|
|
7936
|
-
this.takeOrphanedTracks = (trackLookupPrefix) => {
|
|
7937
|
-
const orphans = this.orphanedTracks.filter((orphan) => orphan.trackLookupPrefix === trackLookupPrefix);
|
|
7938
|
-
if (orphans.length > 0) {
|
|
7939
|
-
this.orphanedTracks = this.orphanedTracks.filter((orphan) => orphan.trackLookupPrefix !== trackLookupPrefix);
|
|
7940
|
-
}
|
|
7941
|
-
return orphans;
|
|
7942
|
-
};
|
|
7943
7518
|
/**
|
|
7944
7519
|
* Updates the call state with the data received from the server.
|
|
7945
7520
|
*
|
|
@@ -7964,43 +7539,6 @@ class CallState {
|
|
|
7964
7539
|
this.setCurrentValue(this.transcribingSubject, call.transcribing);
|
|
7965
7540
|
this.setCurrentValue(this.thumbnailsSubject, call.thumbnails);
|
|
7966
7541
|
};
|
|
7967
|
-
/**
|
|
7968
|
-
* Updates the call state with the data received from the SFU server.
|
|
7969
|
-
*
|
|
7970
|
-
* @internal
|
|
7971
|
-
*
|
|
7972
|
-
* @param callState the call state from the SFU server.
|
|
7973
|
-
* @param currentSessionId the session ID of the current user.
|
|
7974
|
-
* @param reconnectDetails optional reconnect details.
|
|
7975
|
-
*/
|
|
7976
|
-
this.updateFromSfuCallState = (callState, currentSessionId, reconnectDetails) => {
|
|
7977
|
-
const { participants, participantCount, startedAt, pins } = callState;
|
|
7978
|
-
const localPublishedTracks = reconnectDetails?.announcedTracks.map((t) => t.trackType) ?? [];
|
|
7979
|
-
this.setParticipants(() => {
|
|
7980
|
-
const participantLookup = this.getParticipantLookupBySessionId();
|
|
7981
|
-
return participants.map((p) => {
|
|
7982
|
-
// We need to preserve the local state of the participant
|
|
7983
|
-
// (e.g. videoDimension, visibilityState, pinnedAt, etc.)
|
|
7984
|
-
// as it doesn't exist on the server.
|
|
7985
|
-
const existingParticipant = participantLookup[p.sessionId];
|
|
7986
|
-
const isLocalParticipant = p.sessionId === currentSessionId;
|
|
7987
|
-
return Object.assign({}, existingParticipant, p, {
|
|
7988
|
-
isLocalParticipant,
|
|
7989
|
-
publishedTracks: isLocalParticipant
|
|
7990
|
-
? localPublishedTracks
|
|
7991
|
-
: p.publishedTracks,
|
|
7992
|
-
viewportVisibilityState: existingParticipant?.viewportVisibilityState ?? {
|
|
7993
|
-
videoTrack: VisibilityState.UNKNOWN,
|
|
7994
|
-
screenShareTrack: VisibilityState.UNKNOWN,
|
|
7995
|
-
},
|
|
7996
|
-
});
|
|
7997
|
-
});
|
|
7998
|
-
});
|
|
7999
|
-
this.setParticipantCount(participantCount?.total || 0);
|
|
8000
|
-
this.setAnonymousParticipantCount(participantCount?.anonymous || 0);
|
|
8001
|
-
this.setStartedAt(startedAt ? Timestamp.toDate(startedAt) : new Date());
|
|
8002
|
-
this.setServerSidePins(pins);
|
|
8003
|
-
};
|
|
8004
7542
|
this.updateFromMemberRemoved = (event) => {
|
|
8005
7543
|
this.updateFromCallResponse(event.call);
|
|
8006
7544
|
this.setCurrentValue(this.membersSubject, (members) => members.filter((m) => event.members.indexOf(m.user_id) === -1));
|
|
@@ -8712,75 +8250,9 @@ const enableHighQualityAudio = (sdp, trackMid, maxBitrate = 510000) => {
|
|
|
8712
8250
|
return SDP.write(parsedSdp);
|
|
8713
8251
|
};
|
|
8714
8252
|
|
|
8715
|
-
const
|
|
8716
|
-
const [major, minor, patch] = version.split('.');
|
|
8717
|
-
let sdkInfo = {
|
|
8718
|
-
type: SdkType.PLAIN_JAVASCRIPT,
|
|
8719
|
-
major,
|
|
8720
|
-
minor,
|
|
8721
|
-
patch,
|
|
8722
|
-
};
|
|
8723
|
-
let osInfo;
|
|
8724
|
-
let deviceInfo;
|
|
8725
|
-
let webRtcInfo;
|
|
8726
|
-
const setSdkInfo = (info) => {
|
|
8727
|
-
sdkInfo = info;
|
|
8728
|
-
};
|
|
8729
|
-
const getSdkInfo = () => {
|
|
8730
|
-
return sdkInfo;
|
|
8731
|
-
};
|
|
8732
|
-
const setOSInfo = (info) => {
|
|
8733
|
-
osInfo = info;
|
|
8734
|
-
};
|
|
8735
|
-
const getOSInfo = () => {
|
|
8736
|
-
return osInfo;
|
|
8737
|
-
};
|
|
8738
|
-
const setDeviceInfo = (info) => {
|
|
8739
|
-
deviceInfo = info;
|
|
8740
|
-
};
|
|
8741
|
-
const getDeviceInfo = () => {
|
|
8742
|
-
return deviceInfo;
|
|
8743
|
-
};
|
|
8744
|
-
const getWebRTCInfo = () => {
|
|
8745
|
-
return webRtcInfo;
|
|
8746
|
-
};
|
|
8747
|
-
const setWebRTCInfo = (info) => {
|
|
8748
|
-
webRtcInfo = info;
|
|
8749
|
-
};
|
|
8750
|
-
const getClientDetails = () => {
|
|
8751
|
-
if (isReactNative()) {
|
|
8752
|
-
// Since RN doesn't support web, sharing browser info is not required
|
|
8753
|
-
return {
|
|
8754
|
-
sdk: getSdkInfo(),
|
|
8755
|
-
os: getOSInfo(),
|
|
8756
|
-
device: getDeviceInfo(),
|
|
8757
|
-
};
|
|
8758
|
-
}
|
|
8759
|
-
const userAgent = new UAParser(navigator.userAgent);
|
|
8760
|
-
const { browser, os, device, cpu } = userAgent.getResult();
|
|
8761
|
-
return {
|
|
8762
|
-
sdk: getSdkInfo(),
|
|
8763
|
-
browser: {
|
|
8764
|
-
name: browser.name || navigator.userAgent,
|
|
8765
|
-
version: browser.version || '',
|
|
8766
|
-
},
|
|
8767
|
-
os: {
|
|
8768
|
-
name: os.name || '',
|
|
8769
|
-
version: os.version || '',
|
|
8770
|
-
architecture: cpu.architecture || '',
|
|
8771
|
-
},
|
|
8772
|
-
device: {
|
|
8773
|
-
name: [device.vendor, device.model, device.type]
|
|
8774
|
-
.filter(Boolean)
|
|
8775
|
-
.join(' '),
|
|
8776
|
-
version: '',
|
|
8777
|
-
},
|
|
8778
|
-
};
|
|
8779
|
-
};
|
|
8780
|
-
|
|
8253
|
+
const logger$3 = getLogger(['Publisher']);
|
|
8781
8254
|
/**
|
|
8782
8255
|
* The `Publisher` is responsible for publishing/unpublishing media streams to/from the SFU
|
|
8783
|
-
*
|
|
8784
8256
|
* @internal
|
|
8785
8257
|
*/
|
|
8786
8258
|
class Publisher {
|
|
@@ -8805,9 +8277,8 @@ class Publisher {
|
|
|
8805
8277
|
* @param isRedEnabled whether RED is enabled.
|
|
8806
8278
|
* @param iceRestartDelay the delay in milliseconds to wait before restarting ICE once connection goes to `disconnected` state.
|
|
8807
8279
|
* @param onUnrecoverableError a callback to call when an unrecoverable error occurs.
|
|
8808
|
-
* @param logTag the log tag to use.
|
|
8809
8280
|
*/
|
|
8810
|
-
constructor({ connectionConfig, sfuClient, dispatcher, state, isDtxEnabled, isRedEnabled,
|
|
8281
|
+
constructor({ connectionConfig, sfuClient, dispatcher, state, isDtxEnabled, isRedEnabled, iceRestartDelay = 2500, onUnrecoverableError, }) {
|
|
8811
8282
|
this.transceiverRegistry = {
|
|
8812
8283
|
[TrackType.AUDIO]: undefined,
|
|
8813
8284
|
[TrackType.VIDEO]: undefined,
|
|
@@ -8821,7 +8292,7 @@ class Publisher {
|
|
|
8821
8292
|
* This is needed because some browsers (Firefox) don't reliably report
|
|
8822
8293
|
* trackId and `mid` parameters.
|
|
8823
8294
|
*
|
|
8824
|
-
* @
|
|
8295
|
+
* @private
|
|
8825
8296
|
*/
|
|
8826
8297
|
this.transceiverInitOrder = [];
|
|
8827
8298
|
this.trackKindMapping = {
|
|
@@ -8853,7 +8324,7 @@ class Publisher {
|
|
|
8853
8324
|
/**
|
|
8854
8325
|
* Closes the publisher PeerConnection and cleans up the resources.
|
|
8855
8326
|
*/
|
|
8856
|
-
this.close = ({ stopTracks }) => {
|
|
8327
|
+
this.close = ({ stopTracks = true } = {}) => {
|
|
8857
8328
|
if (stopTracks) {
|
|
8858
8329
|
this.stopPublishing();
|
|
8859
8330
|
Object.keys(this.transceiverRegistry).forEach((trackType) => {
|
|
@@ -8865,22 +8336,10 @@ class Publisher {
|
|
|
8865
8336
|
this.trackLayersCache[trackType] = undefined;
|
|
8866
8337
|
});
|
|
8867
8338
|
}
|
|
8868
|
-
this.
|
|
8869
|
-
this.pc.close();
|
|
8870
|
-
};
|
|
8871
|
-
/**
|
|
8872
|
-
* Detaches the event handlers from the `RTCPeerConnection`.
|
|
8873
|
-
* This is useful when we want to replace the `RTCPeerConnection`
|
|
8874
|
-
* instance with a new one (in case of migration).
|
|
8875
|
-
*/
|
|
8876
|
-
this.detachEventHandlers = () => {
|
|
8339
|
+
clearTimeout(this.iceRestartTimeout);
|
|
8877
8340
|
this.unsubscribeOnIceRestart();
|
|
8878
|
-
this.pc.removeEventListener('icecandidate', this.onIceCandidate);
|
|
8879
8341
|
this.pc.removeEventListener('negotiationneeded', this.onNegotiationNeeded);
|
|
8880
|
-
this.pc.
|
|
8881
|
-
this.pc.removeEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
|
|
8882
|
-
this.pc.removeEventListener('icegatheringstatechange', this.onIceGatheringStateChange);
|
|
8883
|
-
this.pc.removeEventListener('signalingstatechange', this.onSignalingStateChange);
|
|
8342
|
+
this.pc.close();
|
|
8884
8343
|
};
|
|
8885
8344
|
/**
|
|
8886
8345
|
* Starts publishing the given track of the given media stream.
|
|
@@ -8907,7 +8366,7 @@ class Publisher {
|
|
|
8907
8366
|
* Once the track has ended, it will notify the SFU and update the state.
|
|
8908
8367
|
*/
|
|
8909
8368
|
const handleTrackEnded = async () => {
|
|
8910
|
-
|
|
8369
|
+
logger$3('info', `Track ${TrackType[trackType]} has ended, notifying the SFU`);
|
|
8911
8370
|
await this.notifyTrackMuteStateChanged(mediaStream, trackType, true);
|
|
8912
8371
|
// clean-up, this event listener needs to run only once.
|
|
8913
8372
|
track.removeEventListener('ended', handleTrackEnded);
|
|
@@ -8923,18 +8382,21 @@ class Publisher {
|
|
|
8923
8382
|
? findOptimalScreenSharingLayers(track, opts.screenShareSettings, screenShareBitrate)
|
|
8924
8383
|
: undefined;
|
|
8925
8384
|
let preferredCodec = opts.preferredCodec;
|
|
8926
|
-
if (!preferredCodec && trackType === TrackType.VIDEO
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
8385
|
+
if (!preferredCodec && trackType === TrackType.VIDEO) {
|
|
8386
|
+
if (isReactNative()) {
|
|
8387
|
+
const osName = getOSInfo()?.name.toLowerCase();
|
|
8388
|
+
if (osName === 'ipados') {
|
|
8389
|
+
// in ipads it was noticed that if vp8 codec is used
|
|
8390
|
+
// then the bytes sent is 0 in the outbound-rtp
|
|
8391
|
+
// so we are forcing h264 codec for ipads
|
|
8392
|
+
preferredCodec = 'H264';
|
|
8393
|
+
}
|
|
8394
|
+
else if (osName === 'android') {
|
|
8395
|
+
preferredCodec = 'VP8';
|
|
8396
|
+
}
|
|
8936
8397
|
}
|
|
8937
8398
|
}
|
|
8399
|
+
const codecPreferences = this.getCodecPreferences(trackType, preferredCodec);
|
|
8938
8400
|
// listen for 'ended' event on the track as it might be ended abruptly
|
|
8939
8401
|
// by an external factor as permission revokes, device disconnected, etc.
|
|
8940
8402
|
// keep in mind that `track.stop()` doesn't trigger this event.
|
|
@@ -8949,20 +8411,17 @@ class Publisher {
|
|
|
8949
8411
|
: undefined,
|
|
8950
8412
|
sendEncodings: videoEncodings,
|
|
8951
8413
|
});
|
|
8952
|
-
|
|
8414
|
+
logger$3('debug', `Added ${TrackType[trackType]} transceiver`);
|
|
8953
8415
|
this.transceiverInitOrder.push(trackType);
|
|
8954
8416
|
this.transceiverRegistry[trackType] = transceiver;
|
|
8955
8417
|
this.publishOptionsPerTrackType.set(trackType, opts);
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
: undefined;
|
|
8959
|
-
if (codecPreferences) {
|
|
8960
|
-
this.logger('info', `Setting ${TrackType[trackType]} codec preferences`, codecPreferences);
|
|
8418
|
+
if ('setCodecPreferences' in transceiver && codecPreferences) {
|
|
8419
|
+
logger$3('info', `Setting ${TrackType[trackType]} codec preferences`, codecPreferences);
|
|
8961
8420
|
try {
|
|
8962
8421
|
transceiver.setCodecPreferences(codecPreferences);
|
|
8963
8422
|
}
|
|
8964
8423
|
catch (err) {
|
|
8965
|
-
|
|
8424
|
+
logger$3('warn', `Couldn't set codec preferences`, err);
|
|
8966
8425
|
}
|
|
8967
8426
|
}
|
|
8968
8427
|
}
|
|
@@ -9011,17 +8470,31 @@ class Publisher {
|
|
|
9011
8470
|
* @param trackType the track type to check.
|
|
9012
8471
|
*/
|
|
9013
8472
|
this.isPublishing = (trackType) => {
|
|
9014
|
-
const
|
|
9015
|
-
if (
|
|
9016
|
-
|
|
9017
|
-
|
|
9018
|
-
|
|
8473
|
+
const transceiverForTrackType = this.transceiverRegistry[trackType];
|
|
8474
|
+
if (transceiverForTrackType && transceiverForTrackType.sender) {
|
|
8475
|
+
const sender = transceiverForTrackType.sender;
|
|
8476
|
+
return (!!sender.track &&
|
|
8477
|
+
sender.track.readyState === 'live' &&
|
|
8478
|
+
sender.track.enabled);
|
|
8479
|
+
}
|
|
8480
|
+
return false;
|
|
8481
|
+
};
|
|
8482
|
+
/**
|
|
8483
|
+
* Returns true if the given track type is currently live
|
|
8484
|
+
*
|
|
8485
|
+
* @param trackType the track type to check.
|
|
8486
|
+
*/
|
|
8487
|
+
this.isLive = (trackType) => {
|
|
8488
|
+
const transceiverForTrackType = this.transceiverRegistry[trackType];
|
|
8489
|
+
if (transceiverForTrackType && transceiverForTrackType.sender) {
|
|
8490
|
+
const sender = transceiverForTrackType.sender;
|
|
8491
|
+
return !!sender.track && sender.track.readyState === 'live';
|
|
8492
|
+
}
|
|
8493
|
+
return false;
|
|
9019
8494
|
};
|
|
9020
8495
|
this.notifyTrackMuteStateChanged = async (mediaStream, trackType, isMuted) => {
|
|
9021
8496
|
await this.sfuClient.updateMuteState(trackType, isMuted);
|
|
9022
8497
|
const audioOrVideoOrScreenShareStream = trackTypeToParticipantStreamKey(trackType);
|
|
9023
|
-
if (!audioOrVideoOrScreenShareStream)
|
|
9024
|
-
return;
|
|
9025
8498
|
if (isMuted) {
|
|
9026
8499
|
this.state.updateParticipant(this.sfuClient.sessionId, (p) => ({
|
|
9027
8500
|
publishedTracks: p.publishedTracks.filter((t) => t !== trackType),
|
|
@@ -9043,7 +8516,7 @@ class Publisher {
|
|
|
9043
8516
|
* Stops publishing all tracks and stop all tracks.
|
|
9044
8517
|
*/
|
|
9045
8518
|
this.stopPublishing = () => {
|
|
9046
|
-
|
|
8519
|
+
logger$3('debug', 'Stopping publishing all tracks');
|
|
9047
8520
|
this.pc.getSenders().forEach((s) => {
|
|
9048
8521
|
s.track?.stop();
|
|
9049
8522
|
if (this.pc.signalingState !== 'closed') {
|
|
@@ -9052,15 +8525,15 @@ class Publisher {
|
|
|
9052
8525
|
});
|
|
9053
8526
|
};
|
|
9054
8527
|
this.updateVideoPublishQuality = async (enabledLayers) => {
|
|
9055
|
-
|
|
8528
|
+
logger$3('info', 'Update publish quality, requested layers by SFU:', enabledLayers);
|
|
9056
8529
|
const videoSender = this.transceiverRegistry[TrackType.VIDEO]?.sender;
|
|
9057
8530
|
if (!videoSender) {
|
|
9058
|
-
|
|
8531
|
+
logger$3('warn', 'Update publish quality, no video sender found.');
|
|
9059
8532
|
return;
|
|
9060
8533
|
}
|
|
9061
8534
|
const params = videoSender.getParameters();
|
|
9062
8535
|
if (params.encodings.length === 0) {
|
|
9063
|
-
|
|
8536
|
+
logger$3('warn', 'Update publish quality, No suitable video encoding quality found');
|
|
9064
8537
|
return;
|
|
9065
8538
|
}
|
|
9066
8539
|
let changed = false;
|
|
@@ -9079,18 +8552,18 @@ class Publisher {
|
|
|
9079
8552
|
if (layer !== undefined) {
|
|
9080
8553
|
if (layer.scaleResolutionDownBy >= 1 &&
|
|
9081
8554
|
layer.scaleResolutionDownBy !== enc.scaleResolutionDownBy) {
|
|
9082
|
-
|
|
8555
|
+
logger$3('debug', '[dynascale]: setting scaleResolutionDownBy from server', 'layer', layer.name, 'scale-resolution-down-by', layer.scaleResolutionDownBy);
|
|
9083
8556
|
enc.scaleResolutionDownBy = layer.scaleResolutionDownBy;
|
|
9084
8557
|
changed = true;
|
|
9085
8558
|
}
|
|
9086
8559
|
if (layer.maxBitrate > 0 && layer.maxBitrate !== enc.maxBitrate) {
|
|
9087
|
-
|
|
8560
|
+
logger$3('debug', '[dynascale] setting max-bitrate from the server', 'layer', layer.name, 'max-bitrate', layer.maxBitrate);
|
|
9088
8561
|
enc.maxBitrate = layer.maxBitrate;
|
|
9089
8562
|
changed = true;
|
|
9090
8563
|
}
|
|
9091
8564
|
if (layer.maxFramerate > 0 &&
|
|
9092
8565
|
layer.maxFramerate !== enc.maxFramerate) {
|
|
9093
|
-
|
|
8566
|
+
logger$3('debug', '[dynascale]: setting maxFramerate from server', 'layer', layer.name, 'max-framerate', layer.maxFramerate);
|
|
9094
8567
|
enc.maxFramerate = layer.maxFramerate;
|
|
9095
8568
|
changed = true;
|
|
9096
8569
|
}
|
|
@@ -9100,10 +8573,10 @@ class Publisher {
|
|
|
9100
8573
|
const activeLayers = params.encodings.filter((e) => e.active);
|
|
9101
8574
|
if (changed) {
|
|
9102
8575
|
await videoSender.setParameters(params);
|
|
9103
|
-
|
|
8576
|
+
logger$3('info', `Update publish quality, enabled rids: `, activeLayers);
|
|
9104
8577
|
}
|
|
9105
8578
|
else {
|
|
9106
|
-
|
|
8579
|
+
logger$3('info', `Update publish quality, no change: `, activeLayers);
|
|
9107
8580
|
}
|
|
9108
8581
|
};
|
|
9109
8582
|
/**
|
|
@@ -9127,7 +8600,7 @@ class Publisher {
|
|
|
9127
8600
|
this.onIceCandidate = (e) => {
|
|
9128
8601
|
const { candidate } = e;
|
|
9129
8602
|
if (!candidate) {
|
|
9130
|
-
|
|
8603
|
+
logger$3('debug', 'null ice candidate');
|
|
9131
8604
|
return;
|
|
9132
8605
|
}
|
|
9133
8606
|
this.sfuClient
|
|
@@ -9136,7 +8609,7 @@ class Publisher {
|
|
|
9136
8609
|
peerType: PeerType.PUBLISHER_UNSPECIFIED,
|
|
9137
8610
|
})
|
|
9138
8611
|
.catch((err) => {
|
|
9139
|
-
|
|
8612
|
+
logger$3('warn', `ICETrickle failed`, err);
|
|
9140
8613
|
});
|
|
9141
8614
|
};
|
|
9142
8615
|
/**
|
|
@@ -9147,23 +8620,38 @@ class Publisher {
|
|
|
9147
8620
|
this.setSfuClient = (sfuClient) => {
|
|
9148
8621
|
this.sfuClient = sfuClient;
|
|
9149
8622
|
};
|
|
8623
|
+
/**
|
|
8624
|
+
* Performs a migration of this publisher instance to a new SFU.
|
|
8625
|
+
*
|
|
8626
|
+
* Initiates a new `iceRestart` offer/answer exchange with the new SFU.
|
|
8627
|
+
*
|
|
8628
|
+
* @param sfuClient the new SFU client to migrate to.
|
|
8629
|
+
* @param connectionConfig the new connection configuration to use.
|
|
8630
|
+
*/
|
|
8631
|
+
this.migrateTo = async (sfuClient, connectionConfig) => {
|
|
8632
|
+
this.sfuClient = sfuClient;
|
|
8633
|
+
this.pc.setConfiguration(connectionConfig);
|
|
8634
|
+
this._connectionConfiguration = connectionConfig;
|
|
8635
|
+
const shouldRestartIce = this.pc.iceConnectionState === 'connected';
|
|
8636
|
+
if (shouldRestartIce) {
|
|
8637
|
+
// negotiate only if there are tracks to publish
|
|
8638
|
+
await this.negotiate({ iceRestart: true });
|
|
8639
|
+
}
|
|
8640
|
+
};
|
|
9150
8641
|
/**
|
|
9151
8642
|
* Restarts the ICE connection and renegotiates with the SFU.
|
|
9152
8643
|
*/
|
|
9153
8644
|
this.restartIce = async () => {
|
|
9154
|
-
|
|
8645
|
+
logger$3('debug', 'Restarting ICE connection');
|
|
9155
8646
|
const signalingState = this.pc.signalingState;
|
|
9156
8647
|
if (this.isIceRestarting || signalingState === 'have-local-offer') {
|
|
9157
|
-
|
|
8648
|
+
logger$3('debug', 'ICE restart is already in progress');
|
|
9158
8649
|
return;
|
|
9159
8650
|
}
|
|
9160
8651
|
await this.negotiate({ iceRestart: true });
|
|
9161
8652
|
};
|
|
9162
8653
|
this.onNegotiationNeeded = () => {
|
|
9163
|
-
this.negotiate().catch((err) =>
|
|
9164
|
-
this.logger('warn', `Negotiation failed.`, err);
|
|
9165
|
-
this.onUnrecoverableError?.();
|
|
9166
|
-
});
|
|
8654
|
+
this.negotiate().catch((err) => logger$3('warn', `Negotiation failed.`, err));
|
|
9167
8655
|
};
|
|
9168
8656
|
/**
|
|
9169
8657
|
* Initiates a new offer/answer exchange with the currently connected SFU.
|
|
@@ -9175,62 +8663,59 @@ class Publisher {
|
|
|
9175
8663
|
const offer = await this.pc.createOffer(options);
|
|
9176
8664
|
let sdp = this.mungeCodecs(offer.sdp);
|
|
9177
8665
|
if (sdp && this.isPublishing(TrackType.SCREEN_SHARE_AUDIO)) {
|
|
9178
|
-
|
|
8666
|
+
const transceiver = this.transceiverRegistry[TrackType.SCREEN_SHARE_AUDIO];
|
|
8667
|
+
if (transceiver && transceiver.sender.track) {
|
|
8668
|
+
const mid = transceiver.mid ??
|
|
8669
|
+
this.extractMid(sdp, transceiver.sender.track, TrackType.SCREEN_SHARE_AUDIO);
|
|
8670
|
+
sdp = enableHighQualityAudio(sdp, mid);
|
|
8671
|
+
}
|
|
9179
8672
|
}
|
|
9180
8673
|
// set the munged SDP back to the offer
|
|
9181
8674
|
offer.sdp = sdp;
|
|
9182
|
-
const trackInfos = this.
|
|
8675
|
+
const trackInfos = this.getCurrentTrackInfos(offer.sdp);
|
|
9183
8676
|
if (trackInfos.length === 0) {
|
|
9184
|
-
throw new Error(`Can't
|
|
8677
|
+
throw new Error(`Can't initiate negotiation without announcing any tracks`);
|
|
9185
8678
|
}
|
|
9186
8679
|
await this.pc.setLocalDescription(offer);
|
|
9187
8680
|
const { response } = await this.sfuClient.setPublisher({
|
|
9188
8681
|
sdp: offer.sdp || '',
|
|
9189
8682
|
tracks: trackInfos,
|
|
9190
8683
|
});
|
|
9191
|
-
const { sdp: remoteSdp, error } = response;
|
|
9192
8684
|
try {
|
|
9193
|
-
await this.pc.setRemoteDescription({
|
|
8685
|
+
await this.pc.setRemoteDescription({
|
|
8686
|
+
type: 'answer',
|
|
8687
|
+
sdp: response.sdp,
|
|
8688
|
+
});
|
|
9194
8689
|
}
|
|
9195
8690
|
catch (e) {
|
|
9196
|
-
|
|
9197
|
-
|
|
9198
|
-
|
|
9199
|
-
|
|
9200
|
-
this.isIceRestarting = false;
|
|
8691
|
+
logger$3('error', `setRemoteDescription error`, {
|
|
8692
|
+
sdp: response.sdp,
|
|
8693
|
+
error: e,
|
|
8694
|
+
});
|
|
9201
8695
|
}
|
|
8696
|
+
this.isIceRestarting = false;
|
|
9202
8697
|
this.sfuClient.iceTrickleBuffer.publisherCandidates.subscribe(async (candidate) => {
|
|
9203
8698
|
try {
|
|
9204
8699
|
const iceCandidate = JSON.parse(candidate.iceCandidate);
|
|
9205
8700
|
await this.pc.addIceCandidate(iceCandidate);
|
|
9206
8701
|
}
|
|
9207
8702
|
catch (e) {
|
|
9208
|
-
|
|
8703
|
+
logger$3('warn', `ICE candidate error`, [e, candidate]);
|
|
9209
8704
|
}
|
|
9210
8705
|
});
|
|
9211
8706
|
};
|
|
9212
|
-
this.enableHighQualityAudio = (sdp) => {
|
|
9213
|
-
const transceiver = this.transceiverRegistry[TrackType.SCREEN_SHARE_AUDIO];
|
|
9214
|
-
if (!transceiver)
|
|
9215
|
-
return sdp;
|
|
9216
|
-
const mid = this.extractMid(transceiver, sdp, TrackType.SCREEN_SHARE_AUDIO);
|
|
9217
|
-
return enableHighQualityAudio(sdp, mid);
|
|
9218
|
-
};
|
|
9219
8707
|
this.mungeCodecs = (sdp) => {
|
|
9220
8708
|
if (sdp) {
|
|
9221
8709
|
sdp = toggleDtx(sdp, this.isDtxEnabled);
|
|
9222
8710
|
}
|
|
9223
8711
|
return sdp;
|
|
9224
8712
|
};
|
|
9225
|
-
this.extractMid = (
|
|
9226
|
-
if (transceiver.mid)
|
|
9227
|
-
return transceiver.mid;
|
|
8713
|
+
this.extractMid = (sdp, track, trackType) => {
|
|
9228
8714
|
if (!sdp) {
|
|
9229
|
-
|
|
8715
|
+
logger$3('warn', 'No SDP found. Returning empty mid');
|
|
9230
8716
|
return '';
|
|
9231
8717
|
}
|
|
9232
|
-
|
|
9233
|
-
const track = transceiver.sender.track;
|
|
8718
|
+
logger$3('debug', `No 'mid' found for track. Trying to find it from the Offer SDP`);
|
|
9234
8719
|
const parsedSdp = SDP.parse(sdp);
|
|
9235
8720
|
const media = parsedSdp.media.find((m) => {
|
|
9236
8721
|
return (m.type === track.kind &&
|
|
@@ -9238,23 +8723,17 @@ class Publisher {
|
|
|
9238
8723
|
(m.msid?.includes(track.id) ?? true));
|
|
9239
8724
|
});
|
|
9240
8725
|
if (typeof media?.mid === 'undefined') {
|
|
9241
|
-
|
|
8726
|
+
logger$3('debug', `No mid found in SDP for track type ${track.kind} and id ${track.id}. Attempting to find a heuristic mid`);
|
|
9242
8727
|
const heuristicMid = this.transceiverInitOrder.indexOf(trackType);
|
|
9243
8728
|
if (heuristicMid !== -1) {
|
|
9244
8729
|
return String(heuristicMid);
|
|
9245
8730
|
}
|
|
9246
|
-
|
|
8731
|
+
logger$3('debug', 'No heuristic mid found. Returning empty mid');
|
|
9247
8732
|
return '';
|
|
9248
8733
|
}
|
|
9249
8734
|
return String(media.mid);
|
|
9250
8735
|
};
|
|
9251
|
-
|
|
9252
|
-
* Returns a list of tracks that are currently being published.
|
|
9253
|
-
*
|
|
9254
|
-
* @internal
|
|
9255
|
-
* @param sdp an optional SDP to extract the `mid` from.
|
|
9256
|
-
*/
|
|
9257
|
-
this.getAnnouncedTracks = (sdp) => {
|
|
8736
|
+
this.getCurrentTrackInfos = (sdp) => {
|
|
9258
8737
|
sdp = sdp || this.pc.localDescription?.sdp;
|
|
9259
8738
|
const { settings } = this.state;
|
|
9260
8739
|
const targetResolution = settings?.video
|
|
@@ -9266,8 +8745,7 @@ class Publisher {
|
|
|
9266
8745
|
const trackType = Number(Object.keys(this.transceiverRegistry).find((key) => this.transceiverRegistry[key] === transceiver));
|
|
9267
8746
|
const track = transceiver.sender.track;
|
|
9268
8747
|
let optimalLayers;
|
|
9269
|
-
|
|
9270
|
-
if (isTrackLive) {
|
|
8748
|
+
if (track.readyState === 'live') {
|
|
9271
8749
|
const publishOpts = this.publishOptionsPerTrackType.get(trackType);
|
|
9272
8750
|
optimalLayers =
|
|
9273
8751
|
trackType === TrackType.VIDEO
|
|
@@ -9280,7 +8758,7 @@ class Publisher {
|
|
|
9280
8758
|
else {
|
|
9281
8759
|
// we report the last known optimal layers for ended tracks
|
|
9282
8760
|
optimalLayers = this.trackLayersCache[trackType] || [];
|
|
9283
|
-
|
|
8761
|
+
logger$3('debug', `Track ${TrackType[trackType]} is ended. Announcing last known optimal layers`, optimalLayers);
|
|
9284
8762
|
}
|
|
9285
8763
|
const layers = optimalLayers.map((optimalLayer) => ({
|
|
9286
8764
|
rid: optimalLayer.rid || '',
|
|
@@ -9302,11 +8780,10 @@ class Publisher {
|
|
|
9302
8780
|
trackId: track.id,
|
|
9303
8781
|
layers: layers,
|
|
9304
8782
|
trackType,
|
|
9305
|
-
mid: this.extractMid(
|
|
8783
|
+
mid: transceiver.mid ?? this.extractMid(sdp, track, trackType),
|
|
9306
8784
|
stereo: isStereo,
|
|
9307
8785
|
dtx: isAudioTrack && this.isDtxEnabled,
|
|
9308
8786
|
red: isAudioTrack && this.isRedEnabled,
|
|
9309
|
-
muted: !isTrackLive,
|
|
9310
8787
|
};
|
|
9311
8788
|
});
|
|
9312
8789
|
};
|
|
@@ -9315,26 +8792,44 @@ class Publisher {
|
|
|
9315
8792
|
`${e.errorCode}: ${e.errorText}`;
|
|
9316
8793
|
const iceState = this.pc.iceConnectionState;
|
|
9317
8794
|
const logLevel = iceState === 'connected' || iceState === 'checking' ? 'debug' : 'warn';
|
|
9318
|
-
|
|
8795
|
+
logger$3(logLevel, `ICE Candidate error`, errorMessage);
|
|
9319
8796
|
};
|
|
9320
8797
|
this.onIceConnectionStateChange = () => {
|
|
9321
8798
|
const state = this.pc.iceConnectionState;
|
|
9322
|
-
|
|
9323
|
-
|
|
9324
|
-
|
|
9325
|
-
|
|
9326
|
-
this.logger('debug', `Attempting to restart ICE`);
|
|
8799
|
+
logger$3('debug', `ICE Connection state changed to`, state);
|
|
8800
|
+
const hasNetworkConnection = this.state.callingState !== CallingState.OFFLINE;
|
|
8801
|
+
if (state === 'failed') {
|
|
8802
|
+
logger$3('debug', `Attempting to restart ICE`);
|
|
9327
8803
|
this.restartIce().catch((e) => {
|
|
9328
|
-
|
|
8804
|
+
logger$3('error', `ICE restart error`, e);
|
|
9329
8805
|
this.onUnrecoverableError?.();
|
|
9330
8806
|
});
|
|
9331
8807
|
}
|
|
8808
|
+
else if (state === 'disconnected' && hasNetworkConnection) {
|
|
8809
|
+
// when in `disconnected` state, the browser may recover automatically,
|
|
8810
|
+
// hence, we delay the ICE restart
|
|
8811
|
+
logger$3('debug', `Scheduling ICE restart in ${this.iceRestartDelay} ms.`);
|
|
8812
|
+
this.iceRestartTimeout = setTimeout(() => {
|
|
8813
|
+
// check if the state is still `disconnected` or `failed`
|
|
8814
|
+
// as the connection may have recovered (or failed) in the meantime
|
|
8815
|
+
if (this.pc.iceConnectionState === 'disconnected' ||
|
|
8816
|
+
this.pc.iceConnectionState === 'failed') {
|
|
8817
|
+
this.restartIce().catch((e) => {
|
|
8818
|
+
logger$3('error', `ICE restart error`, e);
|
|
8819
|
+
this.onUnrecoverableError?.();
|
|
8820
|
+
});
|
|
8821
|
+
}
|
|
8822
|
+
else {
|
|
8823
|
+
logger$3('debug', `Scheduled ICE restart: connection recovered, canceled.`);
|
|
8824
|
+
}
|
|
8825
|
+
}, this.iceRestartDelay);
|
|
8826
|
+
}
|
|
9332
8827
|
};
|
|
9333
8828
|
this.onIceGatheringStateChange = () => {
|
|
9334
|
-
|
|
8829
|
+
logger$3('debug', `ICE Gathering State`, this.pc.iceGatheringState);
|
|
9335
8830
|
};
|
|
9336
8831
|
this.onSignalingStateChange = () => {
|
|
9337
|
-
|
|
8832
|
+
logger$3('debug', `Signaling state changed`, this.pc.signalingState);
|
|
9338
8833
|
};
|
|
9339
8834
|
this.ridToVideoQuality = (rid) => {
|
|
9340
8835
|
return rid === 'q'
|
|
@@ -9343,29 +8838,28 @@ class Publisher {
|
|
|
9343
8838
|
? VideoQuality.MID
|
|
9344
8839
|
: VideoQuality.HIGH; // default to HIGH
|
|
9345
8840
|
};
|
|
9346
|
-
this.logger = getLogger(['Publisher', logTag]);
|
|
9347
8841
|
this.pc = this.createPeerConnection(connectionConfig);
|
|
9348
8842
|
this.sfuClient = sfuClient;
|
|
9349
8843
|
this.state = state;
|
|
9350
8844
|
this.isDtxEnabled = isDtxEnabled;
|
|
9351
8845
|
this.isRedEnabled = isRedEnabled;
|
|
8846
|
+
this.iceRestartDelay = iceRestartDelay;
|
|
9352
8847
|
this.onUnrecoverableError = onUnrecoverableError;
|
|
9353
8848
|
this.unsubscribeOnIceRestart = dispatcher.on('iceRestart', (iceRestart) => {
|
|
9354
8849
|
if (iceRestart.peerType !== PeerType.PUBLISHER_UNSPECIFIED)
|
|
9355
8850
|
return;
|
|
9356
8851
|
this.restartIce().catch((err) => {
|
|
9357
|
-
|
|
8852
|
+
logger$3('warn', `ICERestart failed`, err);
|
|
9358
8853
|
this.onUnrecoverableError?.();
|
|
9359
8854
|
});
|
|
9360
8855
|
});
|
|
9361
8856
|
}
|
|
9362
8857
|
}
|
|
9363
8858
|
|
|
8859
|
+
const logger$2 = getLogger(['Subscriber']);
|
|
9364
8860
|
/**
|
|
9365
8861
|
* A wrapper around the `RTCPeerConnection` that handles the incoming
|
|
9366
8862
|
* media streams from the SFU.
|
|
9367
|
-
*
|
|
9368
|
-
* @internal
|
|
9369
8863
|
*/
|
|
9370
8864
|
class Subscriber {
|
|
9371
8865
|
/**
|
|
@@ -9387,9 +8881,8 @@ class Subscriber {
|
|
|
9387
8881
|
* @param connectionConfig the connection configuration to use.
|
|
9388
8882
|
* @param iceRestartDelay the delay in milliseconds to wait before restarting ICE when connection goes to `disconnected` state.
|
|
9389
8883
|
* @param onUnrecoverableError a callback to call when an unrecoverable error occurs.
|
|
9390
|
-
* @param logTag a tag to use for logging.
|
|
9391
8884
|
*/
|
|
9392
|
-
constructor({ sfuClient, dispatcher, state, connectionConfig,
|
|
8885
|
+
constructor({ sfuClient, dispatcher, state, connectionConfig, iceRestartDelay = 2500, onUnrecoverableError, }) {
|
|
9393
8886
|
this.isIceRestarting = false;
|
|
9394
8887
|
/**
|
|
9395
8888
|
* Creates a new `RTCPeerConnection` instance with the given configuration.
|
|
@@ -9410,22 +8903,10 @@ class Subscriber {
|
|
|
9410
8903
|
* Closes the `RTCPeerConnection` and unsubscribes from the dispatcher.
|
|
9411
8904
|
*/
|
|
9412
8905
|
this.close = () => {
|
|
9413
|
-
this.
|
|
9414
|
-
this.pc.close();
|
|
9415
|
-
};
|
|
9416
|
-
/**
|
|
9417
|
-
* Detaches the event handlers from the `RTCPeerConnection`.
|
|
9418
|
-
* This is useful when we want to replace the `RTCPeerConnection`
|
|
9419
|
-
* instance with a new one (in case of migration).
|
|
9420
|
-
*/
|
|
9421
|
-
this.detachEventHandlers = () => {
|
|
8906
|
+
clearTimeout(this.iceRestartTimeout);
|
|
9422
8907
|
this.unregisterOnSubscriberOffer();
|
|
9423
8908
|
this.unregisterOnIceRestart();
|
|
9424
|
-
this.pc.
|
|
9425
|
-
this.pc.removeEventListener('track', this.handleOnTrack);
|
|
9426
|
-
this.pc.removeEventListener('icecandidateerror', this.onIceCandidateError);
|
|
9427
|
-
this.pc.removeEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
|
|
9428
|
-
this.pc.removeEventListener('icegatheringstatechange', this.onIceGatheringStateChange);
|
|
8909
|
+
this.pc.close();
|
|
9429
8910
|
};
|
|
9430
8911
|
/**
|
|
9431
8912
|
* Returns the result of the `RTCPeerConnection.getStats()` method
|
|
@@ -9443,17 +8924,76 @@ class Subscriber {
|
|
|
9443
8924
|
this.setSfuClient = (sfuClient) => {
|
|
9444
8925
|
this.sfuClient = sfuClient;
|
|
9445
8926
|
};
|
|
8927
|
+
/**
|
|
8928
|
+
* Migrates the subscriber to a new SFU client.
|
|
8929
|
+
*
|
|
8930
|
+
* @param sfuClient the new SFU client to migrate to.
|
|
8931
|
+
* @param connectionConfig the new connection configuration to use.
|
|
8932
|
+
*/
|
|
8933
|
+
this.migrateTo = (sfuClient, connectionConfig) => {
|
|
8934
|
+
this.setSfuClient(sfuClient);
|
|
8935
|
+
// when migrating, we want to keep the previous subscriber open
|
|
8936
|
+
// until the new one is connected
|
|
8937
|
+
const previousPC = this.pc;
|
|
8938
|
+
// we keep a record of previously available video tracks
|
|
8939
|
+
// so that we can monitor when they become available on the new
|
|
8940
|
+
// subscriber and close the previous one.
|
|
8941
|
+
const trackIdsToMigrate = new Set();
|
|
8942
|
+
previousPC.getReceivers().forEach((r) => {
|
|
8943
|
+
if (r.track.kind === 'video') {
|
|
8944
|
+
trackIdsToMigrate.add(r.track.id);
|
|
8945
|
+
}
|
|
8946
|
+
});
|
|
8947
|
+
// set up a new subscriber peer connection, configured to connect
|
|
8948
|
+
// to the new SFU node
|
|
8949
|
+
const pc = this.createPeerConnection(connectionConfig);
|
|
8950
|
+
let migrationTimeoutId;
|
|
8951
|
+
const cleanupMigration = () => {
|
|
8952
|
+
previousPC.close();
|
|
8953
|
+
clearTimeout(migrationTimeoutId);
|
|
8954
|
+
};
|
|
8955
|
+
// When migrating, we want to keep track of the video tracks
|
|
8956
|
+
// that are migrating to the new subscriber.
|
|
8957
|
+
// Once all of them are available, we can close the previous subscriber.
|
|
8958
|
+
const handleTrackMigration = (e) => {
|
|
8959
|
+
logger$2('debug', `[Migration]: Migrated track: ${e.track.id}, ${e.track.kind}`);
|
|
8960
|
+
trackIdsToMigrate.delete(e.track.id);
|
|
8961
|
+
if (trackIdsToMigrate.size === 0) {
|
|
8962
|
+
logger$2('debug', `[Migration]: Migration complete`);
|
|
8963
|
+
pc.removeEventListener('track', handleTrackMigration);
|
|
8964
|
+
cleanupMigration();
|
|
8965
|
+
}
|
|
8966
|
+
};
|
|
8967
|
+
// When migrating, we want to keep track of the connection state
|
|
8968
|
+
// of the new subscriber.
|
|
8969
|
+
// Once it is connected, we give it a 2-second grace period to receive
|
|
8970
|
+
// all the video tracks that are migrating from the previous subscriber.
|
|
8971
|
+
// After this threshold, we abruptly close the previous subscriber.
|
|
8972
|
+
const handleConnectionStateChange = () => {
|
|
8973
|
+
if (pc.connectionState === 'connected') {
|
|
8974
|
+
migrationTimeoutId = setTimeout(() => {
|
|
8975
|
+
pc.removeEventListener('track', handleTrackMigration);
|
|
8976
|
+
cleanupMigration();
|
|
8977
|
+
}, 2000);
|
|
8978
|
+
pc.removeEventListener('connectionstatechange', handleConnectionStateChange);
|
|
8979
|
+
}
|
|
8980
|
+
};
|
|
8981
|
+
pc.addEventListener('track', handleTrackMigration);
|
|
8982
|
+
pc.addEventListener('connectionstatechange', handleConnectionStateChange);
|
|
8983
|
+
// replace the PeerConnection instance
|
|
8984
|
+
this.pc = pc;
|
|
8985
|
+
};
|
|
9446
8986
|
/**
|
|
9447
8987
|
* Restarts the ICE connection and renegotiates with the SFU.
|
|
9448
8988
|
*/
|
|
9449
8989
|
this.restartIce = async () => {
|
|
9450
|
-
|
|
8990
|
+
logger$2('debug', 'Restarting ICE connection');
|
|
9451
8991
|
if (this.pc.signalingState === 'have-remote-offer') {
|
|
9452
|
-
|
|
8992
|
+
logger$2('debug', 'ICE restart is already in progress');
|
|
9453
8993
|
return;
|
|
9454
8994
|
}
|
|
9455
8995
|
if (this.pc.connectionState === 'new') {
|
|
9456
|
-
|
|
8996
|
+
logger$2('debug', `ICE connection is not yet established, skipping restart.`);
|
|
9457
8997
|
return;
|
|
9458
8998
|
}
|
|
9459
8999
|
const previousIsIceRestarting = this.isIceRestarting;
|
|
@@ -9472,42 +9012,36 @@ class Subscriber {
|
|
|
9472
9012
|
this.handleOnTrack = (e) => {
|
|
9473
9013
|
const [primaryStream] = e.streams;
|
|
9474
9014
|
// example: `e3f6aaf8-b03d-4911-be36-83f47d37a76a:TRACK_TYPE_VIDEO`
|
|
9475
|
-
const [trackId,
|
|
9015
|
+
const [trackId, trackType] = primaryStream.id.split(':');
|
|
9476
9016
|
const participantToUpdate = this.state.participants.find((p) => p.trackLookupPrefix === trackId);
|
|
9477
|
-
|
|
9478
|
-
|
|
9017
|
+
logger$2('debug', `[onTrack]: Got remote ${trackType} track for userId: ${participantToUpdate?.userId}`, e.track.id, e.track);
|
|
9018
|
+
if (!participantToUpdate) {
|
|
9019
|
+
logger$2('warn', `[onTrack]: Received track for unknown participant: ${trackId}`, e);
|
|
9020
|
+
return;
|
|
9021
|
+
}
|
|
9022
|
+
const trackDebugInfo = `${participantToUpdate.userId} ${trackType}:${trackId}`;
|
|
9479
9023
|
e.track.addEventListener('mute', () => {
|
|
9480
|
-
|
|
9024
|
+
logger$2('info', `[onTrack]: Track muted: ${trackDebugInfo}`);
|
|
9481
9025
|
});
|
|
9482
9026
|
e.track.addEventListener('unmute', () => {
|
|
9483
|
-
|
|
9027
|
+
logger$2('info', `[onTrack]: Track unmuted: ${trackDebugInfo}`);
|
|
9484
9028
|
});
|
|
9485
9029
|
e.track.addEventListener('ended', () => {
|
|
9486
|
-
|
|
9487
|
-
this.state.removeOrphanedTrack(primaryStream.id);
|
|
9030
|
+
logger$2('info', `[onTrack]: Track ended: ${trackDebugInfo}`);
|
|
9488
9031
|
});
|
|
9489
|
-
const
|
|
9490
|
-
|
|
9491
|
-
|
|
9492
|
-
|
|
9493
|
-
|
|
9494
|
-
|
|
9495
|
-
this.state.registerOrphanedTrack({
|
|
9496
|
-
id: primaryStream.id,
|
|
9497
|
-
trackLookupPrefix: trackId,
|
|
9498
|
-
track: primaryStream,
|
|
9499
|
-
trackType,
|
|
9500
|
-
});
|
|
9501
|
-
return;
|
|
9502
|
-
}
|
|
9503
|
-
const streamKindProp = trackTypeToParticipantStreamKey(trackType);
|
|
9032
|
+
const streamKindProp = {
|
|
9033
|
+
TRACK_TYPE_AUDIO: 'audioStream',
|
|
9034
|
+
TRACK_TYPE_VIDEO: 'videoStream',
|
|
9035
|
+
TRACK_TYPE_SCREEN_SHARE: 'screenShareStream',
|
|
9036
|
+
TRACK_TYPE_SCREEN_SHARE_AUDIO: 'screenShareAudioStream',
|
|
9037
|
+
}[trackType];
|
|
9504
9038
|
if (!streamKindProp) {
|
|
9505
|
-
|
|
9039
|
+
logger$2('error', `Unknown track type: ${trackType}`);
|
|
9506
9040
|
return;
|
|
9507
9041
|
}
|
|
9508
9042
|
const previousStream = participantToUpdate[streamKindProp];
|
|
9509
9043
|
if (previousStream) {
|
|
9510
|
-
|
|
9044
|
+
logger$2('info', `[onTrack]: Cleaning up previous remote ${e.track.kind} tracks for userId: ${participantToUpdate.userId}`);
|
|
9511
9045
|
previousStream.getTracks().forEach((t) => {
|
|
9512
9046
|
t.stop();
|
|
9513
9047
|
previousStream.removeTrack(t);
|
|
@@ -9520,7 +9054,7 @@ class Subscriber {
|
|
|
9520
9054
|
this.onIceCandidate = (e) => {
|
|
9521
9055
|
const { candidate } = e;
|
|
9522
9056
|
if (!candidate) {
|
|
9523
|
-
|
|
9057
|
+
logger$2('debug', 'null ice candidate');
|
|
9524
9058
|
return;
|
|
9525
9059
|
}
|
|
9526
9060
|
this.sfuClient
|
|
@@ -9529,11 +9063,11 @@ class Subscriber {
|
|
|
9529
9063
|
peerType: PeerType.SUBSCRIBER,
|
|
9530
9064
|
})
|
|
9531
9065
|
.catch((err) => {
|
|
9532
|
-
|
|
9066
|
+
logger$2('warn', `ICETrickle failed`, err);
|
|
9533
9067
|
});
|
|
9534
9068
|
};
|
|
9535
9069
|
this.negotiate = async (subscriberOffer) => {
|
|
9536
|
-
|
|
9070
|
+
logger$2('info', `Received subscriberOffer`, subscriberOffer);
|
|
9537
9071
|
await this.pc.setRemoteDescription({
|
|
9538
9072
|
type: 'offer',
|
|
9539
9073
|
sdp: subscriberOffer.sdp,
|
|
@@ -9544,7 +9078,7 @@ class Subscriber {
|
|
|
9544
9078
|
await this.pc.addIceCandidate(iceCandidate);
|
|
9545
9079
|
}
|
|
9546
9080
|
catch (e) {
|
|
9547
|
-
|
|
9081
|
+
logger$2('warn', `ICE candidate error`, [e, candidate]);
|
|
9548
9082
|
}
|
|
9549
9083
|
});
|
|
9550
9084
|
const answer = await this.pc.createAnswer();
|
|
@@ -9557,53 +9091,63 @@ class Subscriber {
|
|
|
9557
9091
|
};
|
|
9558
9092
|
this.onIceConnectionStateChange = () => {
|
|
9559
9093
|
const state = this.pc.iceConnectionState;
|
|
9560
|
-
|
|
9561
|
-
if (this.state.callingState === CallingState.RECONNECTING)
|
|
9562
|
-
return;
|
|
9094
|
+
logger$2('debug', `ICE connection state changed`, state);
|
|
9563
9095
|
// do nothing when ICE is restarting
|
|
9564
9096
|
if (this.isIceRestarting)
|
|
9565
9097
|
return;
|
|
9566
|
-
|
|
9567
|
-
|
|
9098
|
+
const hasNetworkConnection = this.state.callingState !== CallingState.OFFLINE;
|
|
9099
|
+
if (state === 'failed') {
|
|
9100
|
+
logger$2('debug', `Attempting to restart ICE`);
|
|
9568
9101
|
this.restartIce().catch((e) => {
|
|
9569
|
-
|
|
9102
|
+
logger$2('error', `ICE restart failed`, e);
|
|
9570
9103
|
this.onUnrecoverableError?.();
|
|
9571
9104
|
});
|
|
9572
9105
|
}
|
|
9106
|
+
else if (state === 'disconnected' && hasNetworkConnection) {
|
|
9107
|
+
// when in `disconnected` state, the browser may recover automatically,
|
|
9108
|
+
// hence, we delay the ICE restart
|
|
9109
|
+
logger$2('debug', `Scheduling ICE restart in ${this.iceRestartDelay} ms.`);
|
|
9110
|
+
this.iceRestartTimeout = setTimeout(() => {
|
|
9111
|
+
// check if the state is still `disconnected` or `failed`
|
|
9112
|
+
// as the connection may have recovered (or failed) in the meantime
|
|
9113
|
+
if (this.pc.iceConnectionState === 'disconnected' ||
|
|
9114
|
+
this.pc.iceConnectionState === 'failed') {
|
|
9115
|
+
this.restartIce().catch((e) => {
|
|
9116
|
+
logger$2('error', `ICE restart failed`, e);
|
|
9117
|
+
this.onUnrecoverableError?.();
|
|
9118
|
+
});
|
|
9119
|
+
}
|
|
9120
|
+
else {
|
|
9121
|
+
logger$2('debug', `Scheduled ICE restart: connection recovered, canceled.`);
|
|
9122
|
+
}
|
|
9123
|
+
}, this.iceRestartDelay);
|
|
9124
|
+
}
|
|
9573
9125
|
};
|
|
9574
9126
|
this.onIceGatheringStateChange = () => {
|
|
9575
|
-
|
|
9127
|
+
logger$2('debug', `ICE gathering state changed`, this.pc.iceGatheringState);
|
|
9576
9128
|
};
|
|
9577
9129
|
this.onIceCandidateError = (e) => {
|
|
9578
9130
|
const errorMessage = e instanceof RTCPeerConnectionIceErrorEvent &&
|
|
9579
9131
|
`${e.errorCode}: ${e.errorText}`;
|
|
9580
9132
|
const iceState = this.pc.iceConnectionState;
|
|
9581
9133
|
const logLevel = iceState === 'connected' || iceState === 'checking' ? 'debug' : 'warn';
|
|
9582
|
-
|
|
9134
|
+
logger$2(logLevel, `ICE Candidate error`, errorMessage);
|
|
9583
9135
|
};
|
|
9584
|
-
this.logger = getLogger(['Subscriber', logTag]);
|
|
9585
9136
|
this.sfuClient = sfuClient;
|
|
9586
9137
|
this.state = state;
|
|
9138
|
+
this.iceRestartDelay = iceRestartDelay;
|
|
9587
9139
|
this.onUnrecoverableError = onUnrecoverableError;
|
|
9588
9140
|
this.pc = this.createPeerConnection(connectionConfig);
|
|
9589
|
-
const subscriberOfferConcurrencyTag = Symbol('subscriberOffer');
|
|
9590
9141
|
this.unregisterOnSubscriberOffer = dispatcher.on('subscriberOffer', (subscriberOffer) => {
|
|
9591
|
-
|
|
9592
|
-
|
|
9593
|
-
withoutConcurrency(subscriberOfferConcurrencyTag, () => {
|
|
9594
|
-
return this.negotiate(subscriberOffer);
|
|
9595
|
-
}).catch((err) => {
|
|
9596
|
-
this.logger('warn', `Negotiation failed.`, err);
|
|
9142
|
+
this.negotiate(subscriberOffer).catch((err) => {
|
|
9143
|
+
logger$2('warn', `Negotiation failed.`, err);
|
|
9597
9144
|
});
|
|
9598
9145
|
});
|
|
9599
|
-
const iceRestartConcurrencyTag = Symbol('iceRestart');
|
|
9600
9146
|
this.unregisterOnIceRestart = dispatcher.on('iceRestart', (iceRestart) => {
|
|
9601
|
-
|
|
9602
|
-
|
|
9603
|
-
|
|
9604
|
-
|
|
9605
|
-
}).catch((err) => {
|
|
9606
|
-
this.logger('warn', `ICERestart failed`, err);
|
|
9147
|
+
if (iceRestart.peerType !== PeerType.SUBSCRIBER)
|
|
9148
|
+
return;
|
|
9149
|
+
this.restartIce().catch((err) => {
|
|
9150
|
+
logger$2('warn', `ICERestart failed`, err);
|
|
9607
9151
|
this.onUnrecoverableError?.();
|
|
9608
9152
|
});
|
|
9609
9153
|
});
|
|
@@ -9611,8 +9155,8 @@ class Subscriber {
|
|
|
9611
9155
|
}
|
|
9612
9156
|
|
|
9613
9157
|
const createWebSocketSignalChannel = (opts) => {
|
|
9614
|
-
const
|
|
9615
|
-
const
|
|
9158
|
+
const logger = getLogger(['sfu-client']);
|
|
9159
|
+
const { endpoint, onMessage } = opts;
|
|
9616
9160
|
const ws = new WebSocket(endpoint);
|
|
9617
9161
|
ws.binaryType = 'arraybuffer'; // do we need this?
|
|
9618
9162
|
ws.addEventListener('error', (e) => {
|
|
@@ -9638,37 +9182,132 @@ const createWebSocketSignalChannel = (opts) => {
|
|
|
9638
9182
|
return ws;
|
|
9639
9183
|
};
|
|
9640
9184
|
|
|
9185
|
+
const sleep = (m) => new Promise((r) => setTimeout(r, m));
|
|
9186
|
+
function isFunction(value) {
|
|
9187
|
+
return (value &&
|
|
9188
|
+
(Object.prototype.toString.call(value) === '[object Function]' ||
|
|
9189
|
+
'function' === typeof value ||
|
|
9190
|
+
value instanceof Function));
|
|
9191
|
+
}
|
|
9641
9192
|
/**
|
|
9642
|
-
*
|
|
9643
|
-
*
|
|
9644
|
-
* Based on:
|
|
9645
|
-
* - https://github.com/tc39/proposal-promise-with-resolvers/blob/main/polyfills.js
|
|
9193
|
+
* A map of known error codes.
|
|
9646
9194
|
*/
|
|
9647
|
-
const
|
|
9648
|
-
|
|
9649
|
-
|
|
9650
|
-
|
|
9651
|
-
|
|
9652
|
-
reject = _reject;
|
|
9653
|
-
});
|
|
9654
|
-
let isResolved = false;
|
|
9655
|
-
let isRejected = false;
|
|
9656
|
-
const resolver = (value) => {
|
|
9657
|
-
isResolved = true;
|
|
9658
|
-
resolve(value);
|
|
9659
|
-
};
|
|
9660
|
-
const rejecter = (reason) => {
|
|
9661
|
-
isRejected = true;
|
|
9662
|
-
reject(reason);
|
|
9663
|
-
};
|
|
9664
|
-
return {
|
|
9665
|
-
promise,
|
|
9666
|
-
resolve: resolver,
|
|
9667
|
-
reject: rejecter,
|
|
9668
|
-
isResolved,
|
|
9669
|
-
isRejected,
|
|
9670
|
-
};
|
|
9195
|
+
const KnownCodes = {
|
|
9196
|
+
TOKEN_EXPIRED: 40,
|
|
9197
|
+
WS_CLOSED_SUCCESS: 1000,
|
|
9198
|
+
WS_CLOSED_ABRUPTLY: 1006,
|
|
9199
|
+
WS_POLICY_VIOLATION: 1008,
|
|
9671
9200
|
};
|
|
9201
|
+
/**
|
|
9202
|
+
* retryInterval - A retry interval which increases acc to number of failures
|
|
9203
|
+
*
|
|
9204
|
+
* @return {number} Duration to wait in milliseconds
|
|
9205
|
+
*/
|
|
9206
|
+
function retryInterval(numberOfFailures) {
|
|
9207
|
+
// try to reconnect in 0.25-5 seconds (random to spread out the load from failures)
|
|
9208
|
+
const max = Math.min(500 + numberOfFailures * 2000, 5000);
|
|
9209
|
+
const min = Math.min(Math.max(250, (numberOfFailures - 1) * 2000), 5000);
|
|
9210
|
+
return Math.floor(Math.random() * (max - min) + min);
|
|
9211
|
+
}
|
|
9212
|
+
function randomId() {
|
|
9213
|
+
return generateUUIDv4();
|
|
9214
|
+
}
|
|
9215
|
+
function hex(bytes) {
|
|
9216
|
+
let s = '';
|
|
9217
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
9218
|
+
s += bytes[i].toString(16).padStart(2, '0');
|
|
9219
|
+
}
|
|
9220
|
+
return s;
|
|
9221
|
+
}
|
|
9222
|
+
// https://tools.ietf.org/html/rfc4122
|
|
9223
|
+
function generateUUIDv4() {
|
|
9224
|
+
const bytes = getRandomBytes(16);
|
|
9225
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40; // version
|
|
9226
|
+
bytes[8] = (bytes[8] & 0xbf) | 0x80; // variant
|
|
9227
|
+
return (hex(bytes.subarray(0, 4)) +
|
|
9228
|
+
'-' +
|
|
9229
|
+
hex(bytes.subarray(4, 6)) +
|
|
9230
|
+
'-' +
|
|
9231
|
+
hex(bytes.subarray(6, 8)) +
|
|
9232
|
+
'-' +
|
|
9233
|
+
hex(bytes.subarray(8, 10)) +
|
|
9234
|
+
'-' +
|
|
9235
|
+
hex(bytes.subarray(10, 16)));
|
|
9236
|
+
}
|
|
9237
|
+
function getRandomValuesWithMathRandom(bytes) {
|
|
9238
|
+
const max = Math.pow(2, (8 * bytes.byteLength) / bytes.length);
|
|
9239
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
9240
|
+
bytes[i] = Math.random() * max;
|
|
9241
|
+
}
|
|
9242
|
+
}
|
|
9243
|
+
const getRandomValues = (() => {
|
|
9244
|
+
if (typeof crypto !== 'undefined' &&
|
|
9245
|
+
typeof crypto?.getRandomValues !== 'undefined') {
|
|
9246
|
+
return crypto.getRandomValues.bind(crypto);
|
|
9247
|
+
}
|
|
9248
|
+
else if (typeof msCrypto !== 'undefined') {
|
|
9249
|
+
return msCrypto.getRandomValues.bind(msCrypto);
|
|
9250
|
+
}
|
|
9251
|
+
else {
|
|
9252
|
+
return getRandomValuesWithMathRandom;
|
|
9253
|
+
}
|
|
9254
|
+
})();
|
|
9255
|
+
function getRandomBytes(length) {
|
|
9256
|
+
const bytes = new Uint8Array(length);
|
|
9257
|
+
getRandomValues(bytes);
|
|
9258
|
+
return bytes;
|
|
9259
|
+
}
|
|
9260
|
+
function convertErrorToJson(err) {
|
|
9261
|
+
const jsonObj = {};
|
|
9262
|
+
if (!err)
|
|
9263
|
+
return jsonObj;
|
|
9264
|
+
try {
|
|
9265
|
+
Object.getOwnPropertyNames(err).forEach((key) => {
|
|
9266
|
+
jsonObj[key] = Object.getOwnPropertyDescriptor(err, key);
|
|
9267
|
+
});
|
|
9268
|
+
}
|
|
9269
|
+
catch (_) {
|
|
9270
|
+
return {
|
|
9271
|
+
error: 'failed to serialize the error',
|
|
9272
|
+
};
|
|
9273
|
+
}
|
|
9274
|
+
return jsonObj;
|
|
9275
|
+
}
|
|
9276
|
+
/**
|
|
9277
|
+
* isOnline safely return the navigator.online value for browser env
|
|
9278
|
+
* if navigator is not in global object, it always return true
|
|
9279
|
+
*/
|
|
9280
|
+
function isOnline(logger) {
|
|
9281
|
+
const nav = typeof navigator !== 'undefined'
|
|
9282
|
+
? navigator
|
|
9283
|
+
: typeof window !== 'undefined' && window.navigator
|
|
9284
|
+
? window.navigator
|
|
9285
|
+
: undefined;
|
|
9286
|
+
if (!nav) {
|
|
9287
|
+
logger('warn', 'isOnline failed to access window.navigator and assume browser is online');
|
|
9288
|
+
return true;
|
|
9289
|
+
}
|
|
9290
|
+
// RN navigator has undefined for onLine
|
|
9291
|
+
if (typeof nav.onLine !== 'boolean') {
|
|
9292
|
+
return true;
|
|
9293
|
+
}
|
|
9294
|
+
return nav.onLine;
|
|
9295
|
+
}
|
|
9296
|
+
/**
|
|
9297
|
+
* listenForConnectionChanges - Adds an event listener fired on browser going online or offline
|
|
9298
|
+
*/
|
|
9299
|
+
function addConnectionEventListeners(cb) {
|
|
9300
|
+
if (typeof window !== 'undefined' && window.addEventListener) {
|
|
9301
|
+
window.addEventListener('offline', cb);
|
|
9302
|
+
window.addEventListener('online', cb);
|
|
9303
|
+
}
|
|
9304
|
+
}
|
|
9305
|
+
function removeConnectionEventListeners(cb) {
|
|
9306
|
+
if (typeof window !== 'undefined' && window.removeEventListener) {
|
|
9307
|
+
window.removeEventListener('offline', cb);
|
|
9308
|
+
window.removeEventListener('online', cb);
|
|
9309
|
+
}
|
|
9310
|
+
}
|
|
9672
9311
|
|
|
9673
9312
|
/**
|
|
9674
9313
|
* The client used for exchanging information with the SFU.
|
|
@@ -9676,228 +9315,133 @@ const promiseWithResolvers = () => {
|
|
|
9676
9315
|
class StreamSfuClient {
|
|
9677
9316
|
/**
|
|
9678
9317
|
* Constructs a new SFU client.
|
|
9318
|
+
*
|
|
9319
|
+
* @param dispatcher the event dispatcher to use.
|
|
9320
|
+
* @param sfuServer the SFU server to connect to.
|
|
9321
|
+
* @param token the JWT token to use for authentication.
|
|
9322
|
+
* @param sessionId the `sessionId` of the currently connected participant.
|
|
9679
9323
|
*/
|
|
9680
|
-
constructor({ dispatcher,
|
|
9324
|
+
constructor({ dispatcher, sfuServer, token, sessionId, }) {
|
|
9681
9325
|
/**
|
|
9682
9326
|
* A buffer for ICE Candidates that are received before
|
|
9683
|
-
* the
|
|
9327
|
+
* the PeerConnections are ready to handle them.
|
|
9684
9328
|
*/
|
|
9685
9329
|
this.iceTrickleBuffer = new IceTrickleBuffer();
|
|
9686
9330
|
/**
|
|
9687
|
-
*
|
|
9688
|
-
*
|
|
9689
|
-
*/
|
|
9690
|
-
this.isLeaving = false;
|
|
9691
|
-
this.pingIntervalInMs = 10 * 1000;
|
|
9692
|
-
this.unhealthyTimeoutInMs = this.pingIntervalInMs + 5 * 1000;
|
|
9693
|
-
this.restoreWebSocketConcurrencyTag = Symbol('recoverWebSocket');
|
|
9694
|
-
/**
|
|
9695
|
-
* Promise that resolves when the JoinResponse is received.
|
|
9696
|
-
* Rejects after a certain threshold if the response is not received.
|
|
9331
|
+
* A flag indicating whether the client is currently migrating away
|
|
9332
|
+
* from this SFU.
|
|
9697
9333
|
*/
|
|
9698
|
-
this.
|
|
9334
|
+
this.isMigratingAway = false;
|
|
9699
9335
|
/**
|
|
9700
|
-
* A
|
|
9336
|
+
* A flag indicating that the client connection is broken for the current
|
|
9337
|
+
* client and that a fast-reconnect with a new client should be attempted.
|
|
9701
9338
|
*/
|
|
9702
|
-
this.
|
|
9703
|
-
this.
|
|
9704
|
-
|
|
9705
|
-
|
|
9706
|
-
|
|
9707
|
-
|
|
9708
|
-
this.lastMessageTimestamp = new Date();
|
|
9709
|
-
this.scheduleConnectionCheck();
|
|
9710
|
-
this.dispatcher.dispatch(message, this.logTag);
|
|
9711
|
-
},
|
|
9712
|
-
});
|
|
9713
|
-
this.signalWs.addEventListener('close', this.handleWebSocketClose);
|
|
9714
|
-
this.signalWs.addEventListener('error', this.restoreWebSocket);
|
|
9715
|
-
this.signalReady = new Promise((resolve) => {
|
|
9716
|
-
const onOpen = () => {
|
|
9717
|
-
this.signalWs.removeEventListener('open', onOpen);
|
|
9718
|
-
resolve(this.signalWs);
|
|
9719
|
-
};
|
|
9720
|
-
this.signalWs.addEventListener('open', onOpen);
|
|
9721
|
-
});
|
|
9722
|
-
};
|
|
9723
|
-
this.cleanUpWebSocket = () => {
|
|
9724
|
-
this.signalWs.removeEventListener('error', this.restoreWebSocket);
|
|
9725
|
-
this.signalWs.removeEventListener('close', this.handleWebSocketClose);
|
|
9726
|
-
};
|
|
9727
|
-
this.restoreWebSocket = () => {
|
|
9728
|
-
withoutConcurrency(this.restoreWebSocketConcurrencyTag, async () => {
|
|
9729
|
-
this.logger('debug', 'Restoring SFU WS connection');
|
|
9730
|
-
this.cleanUpWebSocket();
|
|
9731
|
-
await sleep(500);
|
|
9732
|
-
this.createWebSocket();
|
|
9733
|
-
}).catch((err) => this.logger('debug', `Can't restore WS connection`, err));
|
|
9734
|
-
};
|
|
9735
|
-
this.handleWebSocketClose = (e) => {
|
|
9736
|
-
this.signalWs.removeEventListener('close', this.handleWebSocketClose);
|
|
9737
|
-
clearInterval(this.keepAliveInterval);
|
|
9738
|
-
clearTimeout(this.connectionCheckTimeout);
|
|
9739
|
-
if (this.onSignalClose) {
|
|
9740
|
-
this.onSignalClose(e);
|
|
9741
|
-
}
|
|
9742
|
-
};
|
|
9743
|
-
this.close = (code = StreamSfuClient.NORMAL_CLOSURE, reason) => {
|
|
9744
|
-
if (this.signalWs.readyState === WebSocket.OPEN) {
|
|
9745
|
-
this.logger('debug', `Closing SFU WS connection: ${code} - ${reason}`);
|
|
9339
|
+
this.isFastReconnecting = false;
|
|
9340
|
+
this.pingIntervalInMs = 10 * 1000;
|
|
9341
|
+
this.unhealthyTimeoutInMs = this.pingIntervalInMs + 5 * 1000;
|
|
9342
|
+
this.close = (code, reason) => {
|
|
9343
|
+
this.logger('debug', `Closing SFU WS connection: ${code} - ${reason}`);
|
|
9344
|
+
if (this.signalWs.readyState !== this.signalWs.CLOSED) {
|
|
9746
9345
|
this.signalWs.close(code, `js-client: ${reason}`);
|
|
9747
|
-
this.cleanUpWebSocket();
|
|
9748
9346
|
}
|
|
9749
|
-
this.dispose();
|
|
9750
|
-
};
|
|
9751
|
-
this.dispose = () => {
|
|
9752
|
-
this.logger('debug', 'Disposing SFU client');
|
|
9753
9347
|
this.unsubscribeIceTrickle();
|
|
9754
9348
|
clearInterval(this.keepAliveInterval);
|
|
9755
9349
|
clearTimeout(this.connectionCheckTimeout);
|
|
9756
|
-
clearTimeout(this.migrateAwayTimeout);
|
|
9757
|
-
this.abortController.abort();
|
|
9758
|
-
this.migrationTask?.resolve();
|
|
9759
|
-
};
|
|
9760
|
-
this.leaveAndClose = async (reason) => {
|
|
9761
|
-
await this.joinResponseTask.promise;
|
|
9762
|
-
try {
|
|
9763
|
-
this.isLeaving = true;
|
|
9764
|
-
await this.notifyLeave(reason);
|
|
9765
|
-
}
|
|
9766
|
-
catch (err) {
|
|
9767
|
-
this.logger('debug', 'Error notifying SFU about leaving call', err);
|
|
9768
|
-
}
|
|
9769
|
-
this.close(StreamSfuClient.NORMAL_CLOSURE, reason.substring(0, 115));
|
|
9770
9350
|
};
|
|
9771
|
-
this.updateSubscriptions = async (
|
|
9772
|
-
|
|
9773
|
-
|
|
9351
|
+
this.updateSubscriptions = async (subscriptions) => {
|
|
9352
|
+
return retryable(() => this.rpc.updateSubscriptions({
|
|
9353
|
+
sessionId: this.sessionId,
|
|
9354
|
+
tracks: subscriptions,
|
|
9355
|
+
}), this.logger, 'debug');
|
|
9774
9356
|
};
|
|
9775
9357
|
this.setPublisher = async (data) => {
|
|
9776
|
-
|
|
9777
|
-
|
|
9358
|
+
return retryable(() => this.rpc.setPublisher({
|
|
9359
|
+
...data,
|
|
9360
|
+
sessionId: this.sessionId,
|
|
9361
|
+
}), this.logger);
|
|
9778
9362
|
};
|
|
9779
9363
|
this.sendAnswer = async (data) => {
|
|
9780
|
-
|
|
9781
|
-
|
|
9364
|
+
return retryable(() => this.rpc.sendAnswer({
|
|
9365
|
+
...data,
|
|
9366
|
+
sessionId: this.sessionId,
|
|
9367
|
+
}), this.logger);
|
|
9782
9368
|
};
|
|
9783
9369
|
this.iceTrickle = async (data) => {
|
|
9784
|
-
|
|
9785
|
-
|
|
9370
|
+
return retryable(() => this.rpc.iceTrickle({
|
|
9371
|
+
...data,
|
|
9372
|
+
sessionId: this.sessionId,
|
|
9373
|
+
}), this.logger);
|
|
9786
9374
|
};
|
|
9787
9375
|
this.iceRestart = async (data) => {
|
|
9788
|
-
|
|
9789
|
-
|
|
9376
|
+
return retryable(() => this.rpc.iceRestart({
|
|
9377
|
+
...data,
|
|
9378
|
+
sessionId: this.sessionId,
|
|
9379
|
+
}), this.logger);
|
|
9790
9380
|
};
|
|
9791
9381
|
this.updateMuteState = async (trackType, muted) => {
|
|
9792
|
-
|
|
9793
|
-
|
|
9382
|
+
return this.updateMuteStates({
|
|
9383
|
+
muteStates: [
|
|
9384
|
+
{
|
|
9385
|
+
trackType,
|
|
9386
|
+
muted,
|
|
9387
|
+
},
|
|
9388
|
+
],
|
|
9389
|
+
});
|
|
9794
9390
|
};
|
|
9795
9391
|
this.updateMuteStates = async (data) => {
|
|
9796
|
-
|
|
9797
|
-
|
|
9392
|
+
return retryable(() => this.rpc.updateMuteStates({
|
|
9393
|
+
...data,
|
|
9394
|
+
sessionId: this.sessionId,
|
|
9395
|
+
}), this.logger);
|
|
9798
9396
|
};
|
|
9799
9397
|
this.sendStats = async (stats) => {
|
|
9800
|
-
|
|
9801
|
-
|
|
9398
|
+
return retryable(() => this.rpc.sendStats({
|
|
9399
|
+
...stats,
|
|
9400
|
+
sessionId: this.sessionId,
|
|
9401
|
+
}), this.logger, 'debug');
|
|
9802
9402
|
};
|
|
9803
9403
|
this.startNoiseCancellation = async () => {
|
|
9804
|
-
|
|
9805
|
-
|
|
9404
|
+
return retryable(() => this.rpc.startNoiseCancellation({
|
|
9405
|
+
sessionId: this.sessionId,
|
|
9406
|
+
}), this.logger);
|
|
9806
9407
|
};
|
|
9807
9408
|
this.stopNoiseCancellation = async () => {
|
|
9808
|
-
|
|
9809
|
-
|
|
9810
|
-
|
|
9811
|
-
this.enterMigration = async (opts = {}) => {
|
|
9812
|
-
this.isLeaving = true;
|
|
9813
|
-
const { timeout = 10000 } = opts;
|
|
9814
|
-
this.migrationTask?.reject(new Error('Cancelled previous migration'));
|
|
9815
|
-
this.migrationTask = promiseWithResolvers();
|
|
9816
|
-
const task = this.migrationTask;
|
|
9817
|
-
const unsubscribe = this.dispatcher.on('participantMigrationComplete', () => {
|
|
9818
|
-
unsubscribe();
|
|
9819
|
-
clearTimeout(this.migrateAwayTimeout);
|
|
9820
|
-
task.resolve();
|
|
9821
|
-
});
|
|
9822
|
-
this.migrateAwayTimeout = setTimeout(() => {
|
|
9823
|
-
unsubscribe();
|
|
9824
|
-
// task.reject(new Error('Migration timeout'));
|
|
9825
|
-
// FIXME OL: temporary, switch to `task.reject()` once the SFU starts sending
|
|
9826
|
-
// the participantMigrationComplete event.
|
|
9827
|
-
task.resolve();
|
|
9828
|
-
}, timeout);
|
|
9829
|
-
return this.migrationTask.promise;
|
|
9409
|
+
return retryable(() => this.rpc.stopNoiseCancellation({
|
|
9410
|
+
sessionId: this.sessionId,
|
|
9411
|
+
}), this.logger);
|
|
9830
9412
|
};
|
|
9831
9413
|
this.join = async (data) => {
|
|
9832
|
-
|
|
9833
|
-
|
|
9834
|
-
|
|
9835
|
-
|
|
9836
|
-
// that's why we have this primitive lock mechanism.
|
|
9837
|
-
// the client starts with already initialized joinResponseTask,
|
|
9838
|
-
// and this code creates a new one for the next join request.
|
|
9839
|
-
this.joinResponseTask = promiseWithResolvers();
|
|
9840
|
-
}
|
|
9841
|
-
// capture a reference to the current joinResponseTask as it might
|
|
9842
|
-
// be replaced with a new one in case a second join request is made
|
|
9843
|
-
const current = this.joinResponseTask;
|
|
9844
|
-
let timeoutId;
|
|
9845
|
-
const unsubscribe = this.dispatcher.on('joinResponse', (joinResponse) => {
|
|
9846
|
-
this.logger('debug', 'Received joinResponse', joinResponse);
|
|
9847
|
-
clearTimeout(timeoutId);
|
|
9848
|
-
unsubscribe();
|
|
9849
|
-
this.keepAlive();
|
|
9850
|
-
current.resolve(joinResponse);
|
|
9414
|
+
const joinRequest = JoinRequest.create({
|
|
9415
|
+
...data,
|
|
9416
|
+
sessionId: this.sessionId,
|
|
9417
|
+
token: this.token,
|
|
9851
9418
|
});
|
|
9852
|
-
timeoutId = setTimeout(() => {
|
|
9853
|
-
unsubscribe();
|
|
9854
|
-
current.reject(new Error('Waiting for "joinResponse" has timed out'));
|
|
9855
|
-
}, this.joinResponseTimeout);
|
|
9856
|
-
await this.send(SfuRequest.create({
|
|
9857
|
-
requestPayload: {
|
|
9858
|
-
oneofKind: 'joinRequest',
|
|
9859
|
-
joinRequest: JoinRequest.create({
|
|
9860
|
-
...data,
|
|
9861
|
-
sessionId: this.sessionId,
|
|
9862
|
-
token: this.credentials.token,
|
|
9863
|
-
}),
|
|
9864
|
-
},
|
|
9865
|
-
}));
|
|
9866
|
-
return current.promise;
|
|
9867
|
-
};
|
|
9868
|
-
this.ping = async () => {
|
|
9869
|
-
return this.send(SfuRequest.create({
|
|
9870
|
-
requestPayload: {
|
|
9871
|
-
oneofKind: 'healthCheckRequest',
|
|
9872
|
-
healthCheckRequest: {},
|
|
9873
|
-
},
|
|
9874
|
-
}));
|
|
9875
|
-
};
|
|
9876
|
-
this.notifyLeave = async (reason) => {
|
|
9877
9419
|
return this.send(SfuRequest.create({
|
|
9878
9420
|
requestPayload: {
|
|
9879
|
-
oneofKind: '
|
|
9880
|
-
|
|
9881
|
-
sessionId: this.sessionId,
|
|
9882
|
-
reason,
|
|
9883
|
-
},
|
|
9421
|
+
oneofKind: 'joinRequest',
|
|
9422
|
+
joinRequest,
|
|
9884
9423
|
},
|
|
9885
9424
|
}));
|
|
9886
9425
|
};
|
|
9887
9426
|
this.send = async (message) => {
|
|
9888
|
-
|
|
9889
|
-
|
|
9890
|
-
|
|
9891
|
-
this.logger('debug',
|
|
9892
|
-
|
|
9893
|
-
}
|
|
9894
|
-
this.logger('debug', `Sending message to: ${this.edgeName}`, msgJson);
|
|
9895
|
-
this.signalWs.send(SfuRequest.toBinary(message));
|
|
9427
|
+
return this.signalReady.then((signal) => {
|
|
9428
|
+
if (signal.readyState !== signal.OPEN)
|
|
9429
|
+
return;
|
|
9430
|
+
this.logger('debug', `Sending message to: ${this.edgeName}`, SfuRequest.toJson(message));
|
|
9431
|
+
signal.send(SfuRequest.toBinary(message));
|
|
9432
|
+
});
|
|
9896
9433
|
};
|
|
9897
9434
|
this.keepAlive = () => {
|
|
9898
9435
|
clearInterval(this.keepAliveInterval);
|
|
9899
9436
|
this.keepAliveInterval = setInterval(() => {
|
|
9900
|
-
this.
|
|
9437
|
+
this.logger('trace', 'Sending healthCheckRequest to SFU');
|
|
9438
|
+
const message = SfuRequest.create({
|
|
9439
|
+
requestPayload: {
|
|
9440
|
+
oneofKind: 'healthCheckRequest',
|
|
9441
|
+
healthCheckRequest: {},
|
|
9442
|
+
},
|
|
9443
|
+
});
|
|
9444
|
+
this.send(message).catch((e) => {
|
|
9901
9445
|
this.logger('error', 'Error sending healthCheckRequest to SFU', e);
|
|
9902
9446
|
});
|
|
9903
9447
|
}, this.pingIntervalInMs);
|
|
@@ -9913,37 +9457,53 @@ class StreamSfuClient {
|
|
|
9913
9457
|
}
|
|
9914
9458
|
}, this.unhealthyTimeoutInMs);
|
|
9915
9459
|
};
|
|
9916
|
-
this.dispatcher = dispatcher;
|
|
9917
9460
|
this.sessionId = sessionId || generateUUIDv4();
|
|
9918
|
-
this.
|
|
9919
|
-
this.
|
|
9920
|
-
|
|
9921
|
-
this.
|
|
9922
|
-
|
|
9923
|
-
|
|
9924
|
-
|
|
9461
|
+
this.sfuServer = sfuServer;
|
|
9462
|
+
this.edgeName = sfuServer.edge_name;
|
|
9463
|
+
this.token = token;
|
|
9464
|
+
this.logger = getLogger(['sfu-client']);
|
|
9465
|
+
const logInterceptor = {
|
|
9466
|
+
interceptUnary: (next, method, input, options) => {
|
|
9467
|
+
this.logger('trace', `Calling SFU RPC method ${method.name}`, {
|
|
9468
|
+
input,
|
|
9469
|
+
options,
|
|
9470
|
+
});
|
|
9471
|
+
return next(method, input, options);
|
|
9472
|
+
},
|
|
9473
|
+
};
|
|
9925
9474
|
this.rpc = createSignalClient({
|
|
9926
|
-
baseUrl:
|
|
9475
|
+
baseUrl: sfuServer.url,
|
|
9927
9476
|
interceptors: [
|
|
9928
9477
|
withHeaders({
|
|
9929
9478
|
Authorization: `Bearer ${token}`,
|
|
9930
9479
|
}),
|
|
9931
|
-
|
|
9932
|
-
]
|
|
9480
|
+
logInterceptor,
|
|
9481
|
+
],
|
|
9933
9482
|
});
|
|
9934
9483
|
// Special handling for the ICETrickle kind of events.
|
|
9935
|
-
//
|
|
9936
|
-
// connection is established
|
|
9937
|
-
//
|
|
9938
|
-
// and later added to the appropriate PeerConnection
|
|
9484
|
+
// These events might be triggered by the SFU before the initial RTC
|
|
9485
|
+
// connection is established. In that case, those events (ICE candidates)
|
|
9486
|
+
// need to be buffered and later added to the appropriate PeerConnection
|
|
9939
9487
|
// once the remoteDescription is known and set.
|
|
9940
9488
|
this.unsubscribeIceTrickle = dispatcher.on('iceTrickle', (iceTrickle) => {
|
|
9941
9489
|
this.iceTrickleBuffer.push(iceTrickle);
|
|
9942
9490
|
});
|
|
9943
|
-
this.
|
|
9944
|
-
|
|
9945
|
-
|
|
9946
|
-
|
|
9491
|
+
this.signalWs = createWebSocketSignalChannel({
|
|
9492
|
+
endpoint: sfuServer.ws_endpoint,
|
|
9493
|
+
onMessage: (message) => {
|
|
9494
|
+
this.lastMessageTimestamp = new Date();
|
|
9495
|
+
this.scheduleConnectionCheck();
|
|
9496
|
+
dispatcher.dispatch(message);
|
|
9497
|
+
},
|
|
9498
|
+
});
|
|
9499
|
+
this.signalReady = new Promise((resolve) => {
|
|
9500
|
+
const onOpen = () => {
|
|
9501
|
+
this.signalWs.removeEventListener('open', onOpen);
|
|
9502
|
+
this.keepAlive();
|
|
9503
|
+
resolve(this.signalWs);
|
|
9504
|
+
};
|
|
9505
|
+
this.signalWs.addEventListener('open', onOpen);
|
|
9506
|
+
});
|
|
9947
9507
|
}
|
|
9948
9508
|
}
|
|
9949
9509
|
/**
|
|
@@ -9956,15 +9516,45 @@ StreamSfuClient.NORMAL_CLOSURE = 1000;
|
|
|
9956
9516
|
* a certain amount of time (`connectionCheckTimeout`).
|
|
9957
9517
|
*/
|
|
9958
9518
|
StreamSfuClient.ERROR_CONNECTION_UNHEALTHY = 4001;
|
|
9959
|
-
|
|
9960
|
-
|
|
9961
|
-
|
|
9962
|
-
|
|
9963
|
-
|
|
9964
|
-
|
|
9965
|
-
|
|
9966
|
-
|
|
9967
|
-
|
|
9519
|
+
/**
|
|
9520
|
+
* The error code used when the SFU connection is broken.
|
|
9521
|
+
* Usually, this means that the WS connection has been closed unexpectedly.
|
|
9522
|
+
* This error code is used to announce a fast-reconnect.
|
|
9523
|
+
*/
|
|
9524
|
+
StreamSfuClient.ERROR_CONNECTION_BROKEN = 4002; // used in fast-reconnects
|
|
9525
|
+
const MAX_RETRIES = 5;
|
|
9526
|
+
/**
|
|
9527
|
+
* Creates a closure which wraps the given RPC call and retries invoking
|
|
9528
|
+
* the RPC until it succeeds or the maximum number of retries is reached.
|
|
9529
|
+
*
|
|
9530
|
+
* Between each retry, there would be a random delay in order to avoid
|
|
9531
|
+
* request bursts towards the SFU.
|
|
9532
|
+
*
|
|
9533
|
+
* @param rpc the closure around the RPC call to execute.
|
|
9534
|
+
* @param logger a logger instance to use.
|
|
9535
|
+
* @param <I> the type of the request object.
|
|
9536
|
+
* @param <O> the type of the response object.
|
|
9537
|
+
*/
|
|
9538
|
+
const retryable = async (rpc, logger, level = 'error') => {
|
|
9539
|
+
let retryAttempt = 0;
|
|
9540
|
+
let rpcCallResult;
|
|
9541
|
+
do {
|
|
9542
|
+
// don't delay the first invocation
|
|
9543
|
+
if (retryAttempt > 0) {
|
|
9544
|
+
await sleep(retryInterval(retryAttempt));
|
|
9545
|
+
}
|
|
9546
|
+
rpcCallResult = await rpc();
|
|
9547
|
+
// if the RPC call failed, log the error and retry
|
|
9548
|
+
if (rpcCallResult.response.error) {
|
|
9549
|
+
logger(level, `SFU RPC Error (${rpcCallResult.method.name}):`, rpcCallResult.response.error);
|
|
9550
|
+
}
|
|
9551
|
+
retryAttempt++;
|
|
9552
|
+
} while (rpcCallResult.response.error?.shouldRetry &&
|
|
9553
|
+
retryAttempt < MAX_RETRIES);
|
|
9554
|
+
if (rpcCallResult.response.error) {
|
|
9555
|
+
throw rpcCallResult.response.error;
|
|
9556
|
+
}
|
|
9557
|
+
return rpcCallResult;
|
|
9968
9558
|
};
|
|
9969
9559
|
|
|
9970
9560
|
/**
|
|
@@ -10119,10 +9709,9 @@ const watchSfuErrorReports = (dispatcher) => {
|
|
|
10119
9709
|
return dispatcher.on('error', (e) => {
|
|
10120
9710
|
if (!e.error)
|
|
10121
9711
|
return;
|
|
10122
|
-
const { error
|
|
9712
|
+
const { error } = e;
|
|
10123
9713
|
logger$1('error', 'SFU reported error', {
|
|
10124
9714
|
code: ErrorCode[error.code],
|
|
10125
|
-
reconnectStrategy: WebsocketReconnectStrategy[reconnectStrategy],
|
|
10126
9715
|
message: error.message,
|
|
10127
9716
|
shouldRetry: error.shouldRetry,
|
|
10128
9717
|
});
|
|
@@ -10138,17 +9727,6 @@ const watchPinsUpdated = (state) => {
|
|
|
10138
9727
|
state.setServerSidePins(pins);
|
|
10139
9728
|
};
|
|
10140
9729
|
};
|
|
10141
|
-
/**
|
|
10142
|
-
* Watches for `callEnded` events.
|
|
10143
|
-
*/
|
|
10144
|
-
const watchSfuCallEnded = (call) => {
|
|
10145
|
-
return call.on('callEnded', (e) => {
|
|
10146
|
-
const reason = CallEndedReason[e.reason];
|
|
10147
|
-
call.leave({ reason }).catch((err) => {
|
|
10148
|
-
logger$1('error', 'Failed to leave call after call ended by the SFU', err);
|
|
10149
|
-
});
|
|
10150
|
-
});
|
|
10151
|
-
};
|
|
10152
9730
|
|
|
10153
9731
|
/**
|
|
10154
9732
|
* An event handler that handles soft mutes.
|
|
@@ -10170,13 +9748,12 @@ const handleRemoteSoftMute = (call) => {
|
|
|
10170
9748
|
else if (type === TrackType.AUDIO) {
|
|
10171
9749
|
await call.microphone.disable();
|
|
10172
9750
|
}
|
|
10173
|
-
else if (type === TrackType.SCREEN_SHARE ||
|
|
10174
|
-
type === TrackType.SCREEN_SHARE_AUDIO) {
|
|
10175
|
-
await call.screenShare.disable();
|
|
10176
|
-
}
|
|
10177
9751
|
else {
|
|
10178
9752
|
logger('warn', 'Unsupported track type to soft mute', TrackType[type]);
|
|
10179
9753
|
}
|
|
9754
|
+
if (call.publisher?.isPublishing(type)) {
|
|
9755
|
+
await call.stopPublish(type);
|
|
9756
|
+
}
|
|
10180
9757
|
}
|
|
10181
9758
|
catch (error) {
|
|
10182
9759
|
logger('error', 'Failed to stop publishing', error);
|
|
@@ -10197,12 +9774,11 @@ const watchParticipantJoined = (state) => {
|
|
|
10197
9774
|
// potential duplicate events from the SFU.
|
|
10198
9775
|
//
|
|
10199
9776
|
// Although the SFU should not send duplicate events, we have seen
|
|
10200
|
-
// some race conditions in the past during the `join-flow
|
|
10201
|
-
//
|
|
9777
|
+
// some race conditions in the past during the `join-flow` where
|
|
9778
|
+
// the SFU would send participant info as part of the `join`
|
|
10202
9779
|
// response and then follow up with a `participantJoined` event for
|
|
10203
9780
|
// already announced participants.
|
|
10204
|
-
|
|
10205
|
-
state.updateOrAddParticipant(participant.sessionId, Object.assign(participant, orphanedTracks, {
|
|
9781
|
+
state.updateOrAddParticipant(participant.sessionId, Object.assign(participant, {
|
|
10206
9782
|
viewportVisibilityState: {
|
|
10207
9783
|
videoTrack: VisibilityState.UNKNOWN,
|
|
10208
9784
|
screenShareTrack: VisibilityState.UNKNOWN,
|
|
@@ -10238,14 +9814,12 @@ const watchParticipantUpdated = (state) => {
|
|
|
10238
9814
|
*/
|
|
10239
9815
|
const watchTrackPublished = (state) => {
|
|
10240
9816
|
return function onTrackPublished(e) {
|
|
10241
|
-
const { type, sessionId } = e;
|
|
9817
|
+
const { type, sessionId, participant } = e;
|
|
10242
9818
|
// An optimization for large calls.
|
|
10243
9819
|
// After a certain threshold, the SFU would stop emitting `participantJoined`
|
|
10244
9820
|
// events, and instead, it would only provide the participant's information
|
|
10245
9821
|
// once they start publishing a track.
|
|
10246
|
-
if (
|
|
10247
|
-
const orphanedTracks = reconcileOrphanedTracks(state, e.participant);
|
|
10248
|
-
const participant = Object.assign(e.participant, orphanedTracks);
|
|
9822
|
+
if (participant) {
|
|
10249
9823
|
state.updateOrAddParticipant(sessionId, participant);
|
|
10250
9824
|
}
|
|
10251
9825
|
else {
|
|
@@ -10261,11 +9835,9 @@ const watchTrackPublished = (state) => {
|
|
|
10261
9835
|
*/
|
|
10262
9836
|
const watchTrackUnpublished = (state) => {
|
|
10263
9837
|
return function onTrackUnpublished(e) {
|
|
10264
|
-
const { type, sessionId } = e;
|
|
9838
|
+
const { type, sessionId, participant } = e;
|
|
10265
9839
|
// An optimization for large calls. See `watchTrackPublished`.
|
|
10266
|
-
if (
|
|
10267
|
-
const orphanedTracks = reconcileOrphanedTracks(state, e.participant);
|
|
10268
|
-
const participant = Object.assign(e.participant, orphanedTracks);
|
|
9840
|
+
if (participant) {
|
|
10269
9841
|
state.updateOrAddParticipant(sessionId, participant);
|
|
10270
9842
|
}
|
|
10271
9843
|
else {
|
|
@@ -10276,25 +9848,6 @@ const watchTrackUnpublished = (state) => {
|
|
|
10276
9848
|
};
|
|
10277
9849
|
};
|
|
10278
9850
|
const unique = (v, i, arr) => arr.indexOf(v) === i;
|
|
10279
|
-
/**
|
|
10280
|
-
* Reconciles orphaned tracks (if any) for the given participant.
|
|
10281
|
-
*
|
|
10282
|
-
* @param state the call state.
|
|
10283
|
-
* @param participant the participant.
|
|
10284
|
-
*/
|
|
10285
|
-
const reconcileOrphanedTracks = (state, participant) => {
|
|
10286
|
-
const orphanTracks = state.takeOrphanedTracks(participant.trackLookupPrefix);
|
|
10287
|
-
if (!orphanTracks.length)
|
|
10288
|
-
return;
|
|
10289
|
-
const reconciledTracks = {};
|
|
10290
|
-
for (const orphan of orphanTracks) {
|
|
10291
|
-
const key = trackTypeToParticipantStreamKey(orphan.trackType);
|
|
10292
|
-
if (!key)
|
|
10293
|
-
continue;
|
|
10294
|
-
reconciledTracks[key] = orphan.track;
|
|
10295
|
-
}
|
|
10296
|
-
return reconciledTracks;
|
|
10297
|
-
};
|
|
10298
9851
|
|
|
10299
9852
|
/**
|
|
10300
9853
|
* Watches for `dominantSpeakerChanged` events.
|
|
@@ -10343,13 +9896,12 @@ const watchAudioLevelChanged = (dispatcher, state) => {
|
|
|
10343
9896
|
* Registers the default event handlers for a call during its lifecycle.
|
|
10344
9897
|
*
|
|
10345
9898
|
* @param call the call to register event handlers for.
|
|
9899
|
+
* @param state the call state.
|
|
10346
9900
|
* @param dispatcher the dispatcher.
|
|
10347
9901
|
*/
|
|
10348
|
-
const registerEventHandlers = (call, dispatcher) => {
|
|
10349
|
-
const state = call.state;
|
|
9902
|
+
const registerEventHandlers = (call, state, dispatcher) => {
|
|
10350
9903
|
const eventHandlers = [
|
|
10351
9904
|
call.on('call.ended', watchCallEnded(call)),
|
|
10352
|
-
watchSfuCallEnded(call),
|
|
10353
9905
|
watchLiveEnded(dispatcher, call),
|
|
10354
9906
|
watchSfuErrorReports(dispatcher),
|
|
10355
9907
|
watchChangePublishQuality(dispatcher, call),
|
|
@@ -10393,6 +9945,48 @@ const registerRingingCallEventHandlers = (call) => {
|
|
|
10393
9945
|
};
|
|
10394
9946
|
};
|
|
10395
9947
|
|
|
9948
|
+
/**
|
|
9949
|
+
* Collects all necessary information to join a call, talks to the coordinator
|
|
9950
|
+
* and returns the necessary information to join the call.
|
|
9951
|
+
*
|
|
9952
|
+
* @param httpClient the http client to use.
|
|
9953
|
+
* @param type the type of the call.
|
|
9954
|
+
* @param id the id of the call.
|
|
9955
|
+
* @param data the data for the call.
|
|
9956
|
+
*/
|
|
9957
|
+
const join = async (httpClient, type, id, data) => {
|
|
9958
|
+
const { call, credentials, members, own_capabilities, stats_options } = await doJoin(httpClient, type, id, data);
|
|
9959
|
+
return {
|
|
9960
|
+
connectionConfig: toRtcConfiguration(credentials.ice_servers),
|
|
9961
|
+
sfuServer: credentials.server,
|
|
9962
|
+
token: credentials.token,
|
|
9963
|
+
metadata: call,
|
|
9964
|
+
members,
|
|
9965
|
+
ownCapabilities: own_capabilities,
|
|
9966
|
+
statsOptions: stats_options,
|
|
9967
|
+
};
|
|
9968
|
+
};
|
|
9969
|
+
const doJoin = async (httpClient, type, id, data) => {
|
|
9970
|
+
const location = await httpClient.getLocationHint();
|
|
9971
|
+
const request = {
|
|
9972
|
+
...data,
|
|
9973
|
+
location,
|
|
9974
|
+
};
|
|
9975
|
+
return httpClient.post(`/call/${type}/${id}/join`, request);
|
|
9976
|
+
};
|
|
9977
|
+
const toRtcConfiguration = (config) => {
|
|
9978
|
+
if (!config || config.length === 0)
|
|
9979
|
+
return undefined;
|
|
9980
|
+
const rtcConfig = {
|
|
9981
|
+
iceServers: config.map((ice) => ({
|
|
9982
|
+
urls: ice.urls,
|
|
9983
|
+
username: ice.username,
|
|
9984
|
+
credential: ice.password,
|
|
9985
|
+
})),
|
|
9986
|
+
};
|
|
9987
|
+
return rtcConfig;
|
|
9988
|
+
};
|
|
9989
|
+
|
|
10396
9990
|
/**
|
|
10397
9991
|
* Flatten the stats report into an array of stats objects.
|
|
10398
9992
|
*
|
|
@@ -10672,7 +10266,6 @@ class SfuStatsReporter {
|
|
|
10672
10266
|
this.start = () => {
|
|
10673
10267
|
if (this.options.reporting_interval_ms <= 0)
|
|
10674
10268
|
return;
|
|
10675
|
-
clearInterval(this.intervalId);
|
|
10676
10269
|
this.intervalId = setInterval(() => {
|
|
10677
10270
|
this.run().catch((err) => {
|
|
10678
10271
|
this.logger('warn', 'Failed to report stats', err);
|
|
@@ -11245,13 +10838,9 @@ class BrowserPermission {
|
|
|
11245
10838
|
const signal = this.disposeController.signal;
|
|
11246
10839
|
this.ready = (async () => {
|
|
11247
10840
|
const assumeGranted = (error) => {
|
|
11248
|
-
this.logger('warn', "Can't query permissions, assuming granted", {
|
|
11249
|
-
permission,
|
|
11250
|
-
error,
|
|
11251
|
-
});
|
|
11252
10841
|
this.setState('granted');
|
|
11253
10842
|
};
|
|
11254
|
-
if (!canQueryPermissions()
|
|
10843
|
+
if (!canQueryPermissions()) {
|
|
11255
10844
|
return assumeGranted();
|
|
11256
10845
|
}
|
|
11257
10846
|
try {
|
|
@@ -11266,7 +10855,7 @@ class BrowserPermission {
|
|
|
11266
10855
|
}
|
|
11267
10856
|
}
|
|
11268
10857
|
catch (err) {
|
|
11269
|
-
assumeGranted(
|
|
10858
|
+
assumeGranted();
|
|
11270
10859
|
}
|
|
11271
10860
|
})();
|
|
11272
10861
|
}
|
|
@@ -11360,7 +10949,7 @@ function lazy(factory) {
|
|
|
11360
10949
|
* Returns an Observable that emits the list of available devices
|
|
11361
10950
|
* that meet the given constraints.
|
|
11362
10951
|
*
|
|
11363
|
-
* @param
|
|
10952
|
+
* @param constraints the constraints to use when requesting the devices.
|
|
11364
10953
|
* @param kind the kind of devices to enumerate.
|
|
11365
10954
|
*/
|
|
11366
10955
|
const getDevices = (permission, kind) => {
|
|
@@ -11613,12 +11202,6 @@ class InputMediaDeviceManager {
|
|
|
11613
11202
|
listDevices() {
|
|
11614
11203
|
return this.getDevices();
|
|
11615
11204
|
}
|
|
11616
|
-
/**
|
|
11617
|
-
* Returns `true` when this device is in enabled state.
|
|
11618
|
-
*/
|
|
11619
|
-
get enabled() {
|
|
11620
|
-
return this.state.status === 'enabled';
|
|
11621
|
-
}
|
|
11622
11205
|
/**
|
|
11623
11206
|
* Starts stream.
|
|
11624
11207
|
*/
|
|
@@ -11741,7 +11324,7 @@ class InputMediaDeviceManager {
|
|
|
11741
11324
|
await this.applySettingsToStream();
|
|
11742
11325
|
}
|
|
11743
11326
|
async applySettingsToStream() {
|
|
11744
|
-
if (this.enabled) {
|
|
11327
|
+
if (this.state.status === 'enabled') {
|
|
11745
11328
|
await this.muteStream();
|
|
11746
11329
|
await this.unmuteStream();
|
|
11747
11330
|
}
|
|
@@ -11878,22 +11461,19 @@ class InputMediaDeviceManager {
|
|
|
11878
11461
|
return output;
|
|
11879
11462
|
})
|
|
11880
11463
|
.then(chainWith(parent), (error) => {
|
|
11881
|
-
this.logger('warn', '
|
|
11464
|
+
this.logger('warn', 'Fitler failed to start and will be ignored', error);
|
|
11882
11465
|
return parent;
|
|
11883
11466
|
}), rootStream);
|
|
11884
11467
|
}
|
|
11885
11468
|
if (this.call.state.callingState === CallingState.JOINED) {
|
|
11886
11469
|
await this.publishStream(stream);
|
|
11887
11470
|
}
|
|
11888
|
-
else {
|
|
11889
|
-
this.logger('debug', 'Stream is not published as the call is not joined');
|
|
11890
|
-
}
|
|
11891
11471
|
if (this.state.mediaStream !== stream) {
|
|
11892
11472
|
this.state.setMediaStream(stream, await rootStream);
|
|
11893
11473
|
this.getTracks().forEach((track) => {
|
|
11894
11474
|
track.addEventListener('ended', async () => {
|
|
11895
11475
|
await this.statusChangeSettled();
|
|
11896
|
-
if (this.enabled) {
|
|
11476
|
+
if (this.state.status === 'enabled') {
|
|
11897
11477
|
this.isTrackStoppedDueToTrackEnd = true;
|
|
11898
11478
|
setTimeout(() => {
|
|
11899
11479
|
this.isTrackStoppedDueToTrackEnd = false;
|
|
@@ -12186,7 +11766,7 @@ class CameraManager extends InputMediaDeviceManager {
|
|
|
12186
11766
|
this.logger('warn', 'could not apply target resolution', error);
|
|
12187
11767
|
}
|
|
12188
11768
|
}
|
|
12189
|
-
if (this.enabled) {
|
|
11769
|
+
if (this.state.status === 'enabled') {
|
|
12190
11770
|
const { width, height } = this.state
|
|
12191
11771
|
.mediaStream.getVideoTracks()[0]
|
|
12192
11772
|
?.getSettings();
|
|
@@ -12490,7 +12070,7 @@ class MicrophoneManager extends InputMediaDeviceManager {
|
|
|
12490
12070
|
});
|
|
12491
12071
|
const registrationResult = this.registerFilter(noiseCancellation.toFilter());
|
|
12492
12072
|
this.noiseCancellationRegistration = registrationResult.registered;
|
|
12493
|
-
this.
|
|
12073
|
+
this.uregisterNoiseCancellation = registrationResult.unregister;
|
|
12494
12074
|
await this.noiseCancellationRegistration;
|
|
12495
12075
|
// handles an edge case where a noise cancellation is enabled after
|
|
12496
12076
|
// the participant as joined the call -> we immediately enable NC
|
|
@@ -12516,7 +12096,7 @@ class MicrophoneManager extends InputMediaDeviceManager {
|
|
|
12516
12096
|
if (isReactNative()) {
|
|
12517
12097
|
throw new Error('Noise cancellation is not supported in React Native');
|
|
12518
12098
|
}
|
|
12519
|
-
await (this.
|
|
12099
|
+
await (this.uregisterNoiseCancellation?.() ?? Promise.resolve())
|
|
12520
12100
|
.then(() => this.noiseCancellation?.disable())
|
|
12521
12101
|
.then(() => this.noiseCancellationChangeUnsubscribe?.())
|
|
12522
12102
|
.catch((err) => {
|
|
@@ -12898,17 +12478,9 @@ class Call {
|
|
|
12898
12478
|
*/
|
|
12899
12479
|
this.dispatcher = new Dispatcher();
|
|
12900
12480
|
this.trackSubscriptionsSubject = new BehaviorSubject({ type: DebounceType.MEDIUM, data: [] });
|
|
12901
|
-
this.sfuClientTag = 0;
|
|
12902
|
-
this.reconnectConcurrencyTag = Symbol('reconnectConcurrencyTag');
|
|
12903
12481
|
this.reconnectAttempts = 0;
|
|
12904
|
-
this.
|
|
12905
|
-
this.
|
|
12906
|
-
this.lastOfflineTimestamp = 0;
|
|
12907
|
-
// maintain the order of publishing tracks to restore them after a reconnection
|
|
12908
|
-
// it shouldn't contain duplicates
|
|
12909
|
-
this.trackPublishOrder = [];
|
|
12910
|
-
this.hasJoinedOnce = false;
|
|
12911
|
-
this.deviceSettingsAppliedOnce = false;
|
|
12482
|
+
this.maxReconnectAttempts = 10;
|
|
12483
|
+
this.isLeaving = false;
|
|
12912
12484
|
this.initialized = false;
|
|
12913
12485
|
this.joinLeaveConcurrencyTag = Symbol('joinLeaveConcurrencyTag');
|
|
12914
12486
|
/**
|
|
@@ -12918,42 +12490,6 @@ class Call {
|
|
|
12918
12490
|
*/
|
|
12919
12491
|
this.leaveCallHooks = new Set();
|
|
12920
12492
|
this.streamClientEventHandlers = new Map();
|
|
12921
|
-
this.handleOwnCapabilitiesUpdated = async (ownCapabilities) => {
|
|
12922
|
-
// update the permission context.
|
|
12923
|
-
this.permissionsContext.setPermissions(ownCapabilities);
|
|
12924
|
-
if (!this.publisher)
|
|
12925
|
-
return;
|
|
12926
|
-
// check if the user still has publishing permissions and stop publishing if not.
|
|
12927
|
-
const permissionToTrackType = {
|
|
12928
|
-
[OwnCapability.SEND_AUDIO]: TrackType.AUDIO,
|
|
12929
|
-
[OwnCapability.SEND_VIDEO]: TrackType.VIDEO,
|
|
12930
|
-
[OwnCapability.SCREENSHARE]: TrackType.SCREEN_SHARE,
|
|
12931
|
-
};
|
|
12932
|
-
for (const [permission, trackType] of Object.entries(permissionToTrackType)) {
|
|
12933
|
-
const hasPermission = this.permissionsContext.hasPermission(permission);
|
|
12934
|
-
if (hasPermission)
|
|
12935
|
-
continue;
|
|
12936
|
-
try {
|
|
12937
|
-
switch (trackType) {
|
|
12938
|
-
case TrackType.AUDIO:
|
|
12939
|
-
if (this.microphone.enabled)
|
|
12940
|
-
await this.microphone.disable();
|
|
12941
|
-
break;
|
|
12942
|
-
case TrackType.VIDEO:
|
|
12943
|
-
if (this.camera.enabled)
|
|
12944
|
-
await this.camera.disable();
|
|
12945
|
-
break;
|
|
12946
|
-
case TrackType.SCREEN_SHARE:
|
|
12947
|
-
if (this.screenShare.enabled)
|
|
12948
|
-
await this.screenShare.disable();
|
|
12949
|
-
break;
|
|
12950
|
-
}
|
|
12951
|
-
}
|
|
12952
|
-
catch (err) {
|
|
12953
|
-
this.logger('error', `Can't disable mic/camera/screenshare after revoked permissions`, err);
|
|
12954
|
-
}
|
|
12955
|
-
}
|
|
12956
|
-
};
|
|
12957
12493
|
/**
|
|
12958
12494
|
* You can subscribe to WebSocket events provided by the API. To remove a subscription, call the `off` method.
|
|
12959
12495
|
* Please note that subscribing to WebSocket events is an advanced use-case.
|
|
@@ -13004,8 +12540,9 @@ class Call {
|
|
|
13004
12540
|
throw new Error('Cannot leave call that has already been left.');
|
|
13005
12541
|
}
|
|
13006
12542
|
if (callingState === CallingState.JOINING) {
|
|
13007
|
-
await this.
|
|
12543
|
+
await this.assertCallJoined();
|
|
13008
12544
|
}
|
|
12545
|
+
this.isLeaving = true;
|
|
13009
12546
|
if (this.ringing) {
|
|
13010
12547
|
// I'm the one who started the call, so I should cancel it.
|
|
13011
12548
|
const hasOtherParticipants = this.state.remoteParticipants.length > 0;
|
|
@@ -13027,15 +12564,14 @@ class Call {
|
|
|
13027
12564
|
this.sfuStatsReporter = undefined;
|
|
13028
12565
|
this.subscriber?.close();
|
|
13029
12566
|
this.subscriber = undefined;
|
|
13030
|
-
this.publisher?.close(
|
|
12567
|
+
this.publisher?.close();
|
|
13031
12568
|
this.publisher = undefined;
|
|
13032
|
-
|
|
12569
|
+
this.sfuClient?.close(StreamSfuClient.NORMAL_CLOSURE, reason);
|
|
13033
12570
|
this.sfuClient = undefined;
|
|
13034
12571
|
this.state.setCallingState(CallingState.LEFT);
|
|
13035
12572
|
// Call all leave call hooks, e.g. to clean up global event handlers
|
|
13036
12573
|
this.leaveCallHooks.forEach((hook) => hook());
|
|
13037
12574
|
this.initialized = false;
|
|
13038
|
-
this.hasJoinedOnce = false;
|
|
13039
12575
|
this.clientStore.unregisterCall(this);
|
|
13040
12576
|
this.camera.dispose();
|
|
13041
12577
|
this.microphone.dispose();
|
|
@@ -13075,7 +12611,7 @@ class Call {
|
|
|
13075
12611
|
this.watching = true;
|
|
13076
12612
|
this.clientStore.registerCall(this);
|
|
13077
12613
|
}
|
|
13078
|
-
await this.applyDeviceConfig(
|
|
12614
|
+
await this.applyDeviceConfig();
|
|
13079
12615
|
return response;
|
|
13080
12616
|
};
|
|
13081
12617
|
/**
|
|
@@ -13097,7 +12633,7 @@ class Call {
|
|
|
13097
12633
|
this.watching = true;
|
|
13098
12634
|
this.clientStore.registerCall(this);
|
|
13099
12635
|
}
|
|
13100
|
-
await this.applyDeviceConfig(
|
|
12636
|
+
await this.applyDeviceConfig();
|
|
13101
12637
|
return response;
|
|
13102
12638
|
};
|
|
13103
12639
|
/**
|
|
@@ -13153,171 +12689,253 @@ class Call {
|
|
|
13153
12689
|
await this.setup();
|
|
13154
12690
|
const callingState = this.state.callingState;
|
|
13155
12691
|
if ([CallingState.JOINED, CallingState.JOINING].includes(callingState)) {
|
|
13156
|
-
|
|
12692
|
+
this.logger('warn', 'Join method called twice, you should only call this once');
|
|
12693
|
+
throw new Error(`Illegal State: Already joined.`);
|
|
13157
12694
|
}
|
|
13158
|
-
|
|
13159
|
-
|
|
12695
|
+
const isMigrating = callingState === CallingState.MIGRATING;
|
|
12696
|
+
const isReconnecting = callingState === CallingState.RECONNECTING;
|
|
13160
12697
|
this.state.setCallingState(CallingState.JOINING);
|
|
13161
|
-
|
|
13162
|
-
|
|
13163
|
-
|
|
13164
|
-
|
|
13165
|
-
if (!this.
|
|
13166
|
-
|
|
13167
|
-
|
|
13168
|
-
|
|
13169
|
-
|
|
13170
|
-
|
|
13171
|
-
|
|
13172
|
-
|
|
12698
|
+
this.logger('debug', 'Starting join flow');
|
|
12699
|
+
if (data?.ring && !this.ringing) {
|
|
12700
|
+
this.ringingSubject.next(true);
|
|
12701
|
+
}
|
|
12702
|
+
if (this.ringing && !this.isCreatedByMe) {
|
|
12703
|
+
// signals other users that I have accepted the incoming call.
|
|
12704
|
+
await this.accept();
|
|
12705
|
+
}
|
|
12706
|
+
let sfuServer;
|
|
12707
|
+
let sfuToken;
|
|
12708
|
+
let connectionConfig;
|
|
12709
|
+
let statsOptions;
|
|
12710
|
+
try {
|
|
12711
|
+
if (this.sfuClient?.isFastReconnecting) {
|
|
12712
|
+
// use previous SFU configuration and values
|
|
12713
|
+
connectionConfig = this.publisher?.connectionConfiguration;
|
|
12714
|
+
sfuServer = this.sfuClient.sfuServer;
|
|
12715
|
+
sfuToken = this.sfuClient.token;
|
|
12716
|
+
statsOptions = this.sfuStatsReporter?.options;
|
|
13173
12717
|
}
|
|
13174
|
-
|
|
13175
|
-
//
|
|
13176
|
-
this.
|
|
13177
|
-
|
|
12718
|
+
else {
|
|
12719
|
+
// full join flow - let the Coordinator pick a new SFU for us
|
|
12720
|
+
const call = await join(this.streamClient, this.type, this.id, data);
|
|
12721
|
+
this.state.updateFromCallResponse(call.metadata);
|
|
12722
|
+
this.state.setMembers(call.members);
|
|
12723
|
+
this.state.setOwnCapabilities(call.ownCapabilities);
|
|
12724
|
+
connectionConfig = call.connectionConfig;
|
|
12725
|
+
sfuServer = call.sfuServer;
|
|
12726
|
+
sfuToken = call.token;
|
|
12727
|
+
statsOptions = call.statsOptions;
|
|
13178
12728
|
}
|
|
13179
|
-
|
|
13180
|
-
|
|
13181
|
-
|
|
13182
|
-
const isWsHealthy = !!previousSfuClient?.isHealthy;
|
|
13183
|
-
const sfuClient = performingRejoin || performingMigration || !isWsHealthy
|
|
13184
|
-
? new StreamSfuClient({
|
|
13185
|
-
logTag: String(this.sfuClientTag++),
|
|
13186
|
-
dispatcher: this.dispatcher,
|
|
13187
|
-
credentials: this.credentials,
|
|
13188
|
-
// a new session_id is necessary for the REJOIN strategy.
|
|
13189
|
-
// we use the previous session_id if available
|
|
13190
|
-
sessionId: performingRejoin ? undefined : previousSessionId,
|
|
13191
|
-
onSignalClose: () => this.handleSfuSignalClose(sfuClient),
|
|
13192
|
-
})
|
|
13193
|
-
: previousSfuClient;
|
|
13194
|
-
this.sfuClient = sfuClient;
|
|
13195
|
-
const clientDetails = getClientDetails();
|
|
13196
|
-
// we don't need to send JoinRequest if we are re-using an existing healthy SFU client
|
|
13197
|
-
if (previousSfuClient !== sfuClient) {
|
|
13198
|
-
// prepare a generic SDP and send it to the SFU.
|
|
13199
|
-
// this is a throw-away SDP that the SFU will use to determine
|
|
13200
|
-
// the capabilities of the client (codec support, etc.)
|
|
13201
|
-
const receivingCapabilitiesSdp = await getGenericSdp('recvonly');
|
|
13202
|
-
const reconnectDetails = this.reconnectStrategy !== WebsocketReconnectStrategy.UNSPECIFIED
|
|
13203
|
-
? this.getReconnectDetails(data?.migrating_from, previousSessionId)
|
|
13204
|
-
: undefined;
|
|
13205
|
-
const { callState, fastReconnectDeadlineSeconds } = await sfuClient.join({
|
|
13206
|
-
subscriberSdp: receivingCapabilitiesSdp,
|
|
13207
|
-
clientDetails,
|
|
13208
|
-
fastReconnect: performingFastReconnect,
|
|
13209
|
-
reconnectDetails,
|
|
13210
|
-
});
|
|
13211
|
-
this.fastReconnectDeadlineSeconds = fastReconnectDeadlineSeconds;
|
|
13212
|
-
if (callState) {
|
|
13213
|
-
this.state.updateFromSfuCallState(callState, sfuClient.sessionId, reconnectDetails);
|
|
12729
|
+
if (this.streamClient._hasConnectionID()) {
|
|
12730
|
+
this.watching = true;
|
|
12731
|
+
this.clientStore.registerCall(this);
|
|
13214
12732
|
}
|
|
13215
12733
|
}
|
|
13216
|
-
|
|
13217
|
-
|
|
13218
|
-
|
|
13219
|
-
|
|
13220
|
-
if (performingFastReconnect) {
|
|
13221
|
-
// the SFU automatically issues an ICE restart on the subscriber
|
|
13222
|
-
// we don't have to do it ourselves
|
|
13223
|
-
await this.restoreICE(sfuClient, { includeSubscriber: false });
|
|
12734
|
+
catch (error) {
|
|
12735
|
+
// restore the previous call state if the join-flow fails
|
|
12736
|
+
this.state.setCallingState(callingState);
|
|
12737
|
+
throw error;
|
|
13224
12738
|
}
|
|
13225
|
-
|
|
13226
|
-
|
|
13227
|
-
this.
|
|
13228
|
-
|
|
13229
|
-
|
|
13230
|
-
|
|
13231
|
-
|
|
13232
|
-
|
|
12739
|
+
const previousSfuClient = this.sfuClient;
|
|
12740
|
+
const sfuClient = (this.sfuClient = new StreamSfuClient({
|
|
12741
|
+
dispatcher: this.dispatcher,
|
|
12742
|
+
sfuServer,
|
|
12743
|
+
token: sfuToken,
|
|
12744
|
+
sessionId: previousSfuClient?.sessionId,
|
|
12745
|
+
}));
|
|
12746
|
+
/**
|
|
12747
|
+
* A closure which hides away the re-connection logic.
|
|
12748
|
+
*/
|
|
12749
|
+
const reconnect = async (strategy, reason) => {
|
|
12750
|
+
const currentState = this.state.callingState;
|
|
12751
|
+
if (currentState === CallingState.MIGRATING ||
|
|
12752
|
+
currentState === CallingState.RECONNECTING) {
|
|
12753
|
+
// prevent parallel reconnection attempts
|
|
12754
|
+
return;
|
|
12755
|
+
}
|
|
12756
|
+
this.reconnectAttempts++;
|
|
12757
|
+
this.state.setCallingState(strategy === 'migrate'
|
|
12758
|
+
? CallingState.MIGRATING
|
|
12759
|
+
: CallingState.RECONNECTING);
|
|
12760
|
+
if (strategy === 'migrate') {
|
|
12761
|
+
this.logger('debug', `[Migration]: migrating call ${this.cid} away from ${sfuServer.edge_name}`);
|
|
12762
|
+
sfuClient.isMigratingAway = true;
|
|
12763
|
+
}
|
|
12764
|
+
else {
|
|
12765
|
+
this.logger('debug', `[Rejoin]: ${strategy} rejoin call ${this.cid} (${this.reconnectAttempts})...`);
|
|
12766
|
+
}
|
|
12767
|
+
// take a snapshot of the current "local participant" state
|
|
12768
|
+
// we'll need it for restoring the previous publishing state later
|
|
12769
|
+
const localParticipant = this.state.localParticipant;
|
|
12770
|
+
if (strategy === 'fast') {
|
|
12771
|
+
sfuClient.close(StreamSfuClient.ERROR_CONNECTION_BROKEN, `attempting fast reconnect: ${reason}`);
|
|
12772
|
+
}
|
|
12773
|
+
else if (strategy === 'full') {
|
|
12774
|
+
// in migration or recovery scenarios, we don't want to
|
|
12775
|
+
// wait before attempting to reconnect to an SFU server
|
|
12776
|
+
await sleep(retryInterval(this.reconnectAttempts));
|
|
12777
|
+
// in full-reconnect, we need to dispose all Peer Connections
|
|
12778
|
+
this.subscriber?.close();
|
|
12779
|
+
this.subscriber = undefined;
|
|
12780
|
+
this.publisher?.close({ stopTracks: false });
|
|
12781
|
+
this.publisher = undefined;
|
|
12782
|
+
this.statsReporter?.stop();
|
|
12783
|
+
this.statsReporter = undefined;
|
|
12784
|
+
this.sfuStatsReporter?.stop();
|
|
12785
|
+
this.sfuStatsReporter = undefined;
|
|
12786
|
+
// clean up current connection
|
|
12787
|
+
sfuClient.close(StreamSfuClient.NORMAL_CLOSURE, `attempting full reconnect: ${reason}`);
|
|
12788
|
+
}
|
|
12789
|
+
await this.join({
|
|
12790
|
+
...data,
|
|
12791
|
+
...(strategy === 'migrate' && { migrating_from: sfuServer.edge_name }),
|
|
13233
12792
|
});
|
|
13234
|
-
|
|
13235
|
-
|
|
13236
|
-
|
|
13237
|
-
await previousSfuClient?.leaveAndClose(`Closing previous WS after reconnect with strategy: ${strategy}`);
|
|
13238
|
-
}
|
|
13239
|
-
else if (!isWsHealthy) {
|
|
13240
|
-
previousSfuClient?.close(4002, 'Closing unhealthy WS after reconnect');
|
|
13241
|
-
}
|
|
13242
|
-
// device settings should be applied only once, we don't have to
|
|
13243
|
-
// re-apply them on later reconnections or server-side data fetches
|
|
13244
|
-
if (!this.deviceSettingsAppliedOnce) {
|
|
13245
|
-
await this.applyDeviceConfig(true);
|
|
13246
|
-
this.deviceSettingsAppliedOnce = true;
|
|
13247
|
-
}
|
|
13248
|
-
this.logger('info', `Joined call ${this.cid}`);
|
|
13249
|
-
};
|
|
13250
|
-
/**
|
|
13251
|
-
* Prepares Reconnect Details object.
|
|
13252
|
-
* @internal
|
|
13253
|
-
*/
|
|
13254
|
-
this.getReconnectDetails = (migratingFromSfuId, previousSessionId) => {
|
|
13255
|
-
const strategy = this.reconnectStrategy;
|
|
13256
|
-
const performingRejoin = strategy === WebsocketReconnectStrategy.REJOIN;
|
|
13257
|
-
const announcedTracks = this.publisher?.getAnnouncedTracks() || [];
|
|
13258
|
-
const subscribedTracks = getCurrentValue(this.trackSubscriptionsSubject);
|
|
13259
|
-
return {
|
|
13260
|
-
strategy,
|
|
13261
|
-
announcedTracks,
|
|
13262
|
-
subscriptions: subscribedTracks.data || [],
|
|
13263
|
-
reconnectAttempt: this.reconnectAttempts,
|
|
13264
|
-
fromSfuId: migratingFromSfuId || '',
|
|
13265
|
-
previousSessionId: performingRejoin ? previousSessionId || '' : '',
|
|
13266
|
-
};
|
|
13267
|
-
};
|
|
13268
|
-
/**
|
|
13269
|
-
* Performs an ICE restart on both the Publisher and Subscriber Peer Connections.
|
|
13270
|
-
* Uses the provided SFU client to restore the ICE connection.
|
|
13271
|
-
*
|
|
13272
|
-
* This method can throw an error if the ICE restart fails.
|
|
13273
|
-
* This error should be handled by the reconnect loop,
|
|
13274
|
-
* and a new reconnection shall be attempted.
|
|
13275
|
-
*
|
|
13276
|
-
* @internal
|
|
13277
|
-
*/
|
|
13278
|
-
this.restoreICE = async (nextSfuClient, opts = {}) => {
|
|
13279
|
-
const { includeSubscriber = true, includePublisher = true } = opts;
|
|
13280
|
-
if (this.subscriber) {
|
|
13281
|
-
this.subscriber.setSfuClient(nextSfuClient);
|
|
13282
|
-
if (includeSubscriber) {
|
|
13283
|
-
await this.subscriber.restartIce();
|
|
12793
|
+
// clean up previous connection
|
|
12794
|
+
if (strategy === 'migrate') {
|
|
12795
|
+
sfuClient.close(StreamSfuClient.NORMAL_CLOSURE, 'attempting migration');
|
|
13284
12796
|
}
|
|
13285
|
-
|
|
13286
|
-
|
|
13287
|
-
|
|
13288
|
-
|
|
13289
|
-
|
|
12797
|
+
this.logger('info', `[Rejoin]: Attempt ${this.reconnectAttempts} successful!`);
|
|
12798
|
+
// we shouldn't be republishing the streams if we're migrating
|
|
12799
|
+
// as the underlying peer connection will take care of it as part
|
|
12800
|
+
// of the ice-restart process
|
|
12801
|
+
if (localParticipant && strategy === 'full') {
|
|
12802
|
+
const { audioStream, videoStream, screenShareStream, screenShareAudioStream, } = localParticipant;
|
|
12803
|
+
let screenShare;
|
|
12804
|
+
if (screenShareStream || screenShareAudioStream) {
|
|
12805
|
+
screenShare = new MediaStream();
|
|
12806
|
+
screenShareStream?.getVideoTracks().forEach((track) => {
|
|
12807
|
+
screenShare?.addTrack(track);
|
|
12808
|
+
});
|
|
12809
|
+
screenShareAudioStream?.getAudioTracks().forEach((track) => {
|
|
12810
|
+
screenShare?.addTrack(track);
|
|
12811
|
+
});
|
|
12812
|
+
}
|
|
12813
|
+
// restore previous publishing state
|
|
12814
|
+
if (audioStream)
|
|
12815
|
+
await this.publishAudioStream(audioStream);
|
|
12816
|
+
if (videoStream) {
|
|
12817
|
+
await this.publishVideoStream(videoStream, {
|
|
12818
|
+
preferredCodec: this.camera.preferredCodec,
|
|
12819
|
+
});
|
|
12820
|
+
}
|
|
12821
|
+
if (screenShare)
|
|
12822
|
+
await this.publishScreenShareStream(screenShare);
|
|
12823
|
+
this.logger('info', `[Rejoin]: State restored. Attempt: ${this.reconnectAttempts}`);
|
|
13290
12824
|
}
|
|
13291
|
-
}
|
|
13292
|
-
|
|
13293
|
-
|
|
13294
|
-
|
|
13295
|
-
|
|
13296
|
-
|
|
13297
|
-
|
|
13298
|
-
|
|
13299
|
-
|
|
13300
|
-
|
|
13301
|
-
|
|
13302
|
-
this.subscriber = new Subscriber({
|
|
13303
|
-
sfuClient,
|
|
13304
|
-
dispatcher: this.dispatcher,
|
|
13305
|
-
state: this.state,
|
|
13306
|
-
connectionConfig,
|
|
13307
|
-
logTag: String(this.reconnectAttempts),
|
|
13308
|
-
onUnrecoverableError: () => {
|
|
13309
|
-
this.reconnect(WebsocketReconnectStrategy.REJOIN).catch((err) => {
|
|
13310
|
-
this.logger('warn', '[Reconnect] Error reconnecting after a subscriber error', err);
|
|
12825
|
+
};
|
|
12826
|
+
// reconnect if the connection was closed unexpectedly. example:
|
|
12827
|
+
// - SFU crash or restart
|
|
12828
|
+
// - network change
|
|
12829
|
+
sfuClient.signalReady.then(() => {
|
|
12830
|
+
// register a handler for the "goAway" event
|
|
12831
|
+
const unregisterGoAway = this.dispatcher.on('goAway', (event) => {
|
|
12832
|
+
const { reason } = event;
|
|
12833
|
+
this.logger('info', `[Migration]: Going away from SFU... Reason: ${GoAwayReason[reason]}`);
|
|
12834
|
+
reconnect('migrate', GoAwayReason[reason]).catch((err) => {
|
|
12835
|
+
this.logger('warn', `[Migration]: Failed to migrate to another SFU.`, err);
|
|
13311
12836
|
});
|
|
13312
|
-
}
|
|
12837
|
+
});
|
|
12838
|
+
sfuClient.signalWs.addEventListener('close', (e) => {
|
|
12839
|
+
// unregister the "goAway" handler, as we won't need it anymore for this connection.
|
|
12840
|
+
// the upcoming re-join will register a new handler anyway
|
|
12841
|
+
unregisterGoAway();
|
|
12842
|
+
// when the user has initiated "call.leave()" operation, we shouldn't
|
|
12843
|
+
// care for the WS close code and we shouldn't ever attempt to reconnect
|
|
12844
|
+
if (this.isLeaving)
|
|
12845
|
+
return;
|
|
12846
|
+
// do nothing if the connection was closed on purpose
|
|
12847
|
+
if (e.code === StreamSfuClient.NORMAL_CLOSURE)
|
|
12848
|
+
return;
|
|
12849
|
+
// do nothing if the connection was closed because of a policy violation
|
|
12850
|
+
// e.g., the user has been blocked by an admin or moderator
|
|
12851
|
+
if (e.code === KnownCodes.WS_POLICY_VIOLATION)
|
|
12852
|
+
return;
|
|
12853
|
+
// When the SFU is being shut down, it sends a goAway message.
|
|
12854
|
+
// While we migrate to another SFU, we might have the WS connection
|
|
12855
|
+
// to the old SFU closed abruptly. In this case, we don't want
|
|
12856
|
+
// to reconnect to the old SFU, but rather to the new one.
|
|
12857
|
+
const isMigratingAway = e.code === KnownCodes.WS_CLOSED_ABRUPTLY && sfuClient.isMigratingAway;
|
|
12858
|
+
const isFastReconnecting = e.code === KnownCodes.WS_CLOSED_ABRUPTLY &&
|
|
12859
|
+
sfuClient.isFastReconnecting;
|
|
12860
|
+
if (isMigratingAway || isFastReconnecting)
|
|
12861
|
+
return;
|
|
12862
|
+
// do nothing if the connection was closed because of a fast reconnect
|
|
12863
|
+
if (e.code === StreamSfuClient.ERROR_CONNECTION_BROKEN)
|
|
12864
|
+
return;
|
|
12865
|
+
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
12866
|
+
sfuClient.isFastReconnecting = this.reconnectAttempts === 0;
|
|
12867
|
+
const strategy = sfuClient.isFastReconnecting ? 'fast' : 'full';
|
|
12868
|
+
reconnect(strategy, `SFU closed the WS with code: ${e.code}`).catch((err) => {
|
|
12869
|
+
this.logger('error', `[Rejoin]: ${strategy} rejoin failed for ${this.reconnectAttempts} times. Giving up.`, err);
|
|
12870
|
+
this.state.setCallingState(CallingState.RECONNECTING_FAILED);
|
|
12871
|
+
});
|
|
12872
|
+
}
|
|
12873
|
+
else {
|
|
12874
|
+
this.logger('error', '[Rejoin]: Reconnect attempts exceeded. Giving up...');
|
|
12875
|
+
this.state.setCallingState(CallingState.RECONNECTING_FAILED);
|
|
12876
|
+
}
|
|
12877
|
+
});
|
|
12878
|
+
});
|
|
12879
|
+
// handlers for connection online/offline events
|
|
12880
|
+
const unsubscribeOnlineEvent = this.streamClient.on('connection.changed', async (e) => {
|
|
12881
|
+
if (e.type !== 'connection.changed')
|
|
12882
|
+
return;
|
|
12883
|
+
if (!e.online)
|
|
12884
|
+
return;
|
|
12885
|
+
unsubscribeOnlineEvent();
|
|
12886
|
+
const currentCallingState = this.state.callingState;
|
|
12887
|
+
const shouldReconnect = currentCallingState === CallingState.OFFLINE ||
|
|
12888
|
+
currentCallingState === CallingState.RECONNECTING_FAILED;
|
|
12889
|
+
if (!shouldReconnect)
|
|
12890
|
+
return;
|
|
12891
|
+
this.logger('info', '[Rejoin]: Going online...');
|
|
12892
|
+
let isFirstReconnectAttempt = true;
|
|
12893
|
+
do {
|
|
12894
|
+
try {
|
|
12895
|
+
sfuClient.isFastReconnecting = isFirstReconnectAttempt;
|
|
12896
|
+
await reconnect(isFirstReconnectAttempt ? 'fast' : 'full', 'Network: online');
|
|
12897
|
+
return; // break the loop if rejoin is successful
|
|
12898
|
+
}
|
|
12899
|
+
catch (err) {
|
|
12900
|
+
this.logger('error', `[Rejoin][Network]: Rejoin failed for attempt ${this.reconnectAttempts}`, err);
|
|
12901
|
+
}
|
|
12902
|
+
// wait for a bit before trying to reconnect again
|
|
12903
|
+
await sleep(retryInterval(this.reconnectAttempts));
|
|
12904
|
+
isFirstReconnectAttempt = false;
|
|
12905
|
+
} while (this.reconnectAttempts < this.maxReconnectAttempts);
|
|
12906
|
+
// if we're here, it means that we've exhausted all the reconnect attempts
|
|
12907
|
+
this.logger('error', `[Rejoin][Network]: Rejoin failed. Giving up.`);
|
|
12908
|
+
this.state.setCallingState(CallingState.RECONNECTING_FAILED);
|
|
12909
|
+
});
|
|
12910
|
+
const unsubscribeOfflineEvent = this.streamClient.on('connection.changed', (e) => {
|
|
12911
|
+
if (e.type !== 'connection.changed')
|
|
12912
|
+
return;
|
|
12913
|
+
if (e.online)
|
|
12914
|
+
return;
|
|
12915
|
+
unsubscribeOfflineEvent();
|
|
12916
|
+
this.state.setCallingState(CallingState.OFFLINE);
|
|
13313
12917
|
});
|
|
12918
|
+
this.leaveCallHooks.add(() => {
|
|
12919
|
+
unsubscribeOnlineEvent();
|
|
12920
|
+
unsubscribeOfflineEvent();
|
|
12921
|
+
});
|
|
12922
|
+
if (!this.subscriber) {
|
|
12923
|
+
this.subscriber = new Subscriber({
|
|
12924
|
+
sfuClient,
|
|
12925
|
+
dispatcher: this.dispatcher,
|
|
12926
|
+
state: this.state,
|
|
12927
|
+
connectionConfig,
|
|
12928
|
+
onUnrecoverableError: () => {
|
|
12929
|
+
reconnect('full', 'unrecoverable subscriber error').catch((err) => {
|
|
12930
|
+
this.logger('debug', '[Rejoin]: Rejoin failed', err);
|
|
12931
|
+
});
|
|
12932
|
+
},
|
|
12933
|
+
});
|
|
12934
|
+
}
|
|
13314
12935
|
// anonymous users can't publish anything hence, there is no need
|
|
13315
12936
|
// to create Publisher Peer Connection for them
|
|
13316
12937
|
const isAnonymous = this.streamClient.user?.type === 'anonymous';
|
|
13317
|
-
if (!isAnonymous) {
|
|
13318
|
-
if (closePreviousInstances && this.publisher) {
|
|
13319
|
-
this.publisher.close({ stopTracks: false });
|
|
13320
|
-
}
|
|
12938
|
+
if (!this.publisher && !isAnonymous) {
|
|
13321
12939
|
const audioSettings = this.state.settings?.audio;
|
|
13322
12940
|
const isDtxEnabled = !!audioSettings?.opus_dtx_enabled;
|
|
13323
12941
|
const isRedEnabled = !!audioSettings?.redundant_coding_enabled;
|
|
@@ -13328,23 +12946,23 @@ class Call {
|
|
|
13328
12946
|
connectionConfig,
|
|
13329
12947
|
isDtxEnabled,
|
|
13330
12948
|
isRedEnabled,
|
|
13331
|
-
logTag: String(this.reconnectAttempts),
|
|
13332
12949
|
onUnrecoverableError: () => {
|
|
13333
|
-
|
|
13334
|
-
this.logger('
|
|
12950
|
+
reconnect('full', 'unrecoverable publisher error').catch((err) => {
|
|
12951
|
+
this.logger('debug', '[Rejoin]: Rejoin failed', err);
|
|
13335
12952
|
});
|
|
13336
12953
|
},
|
|
13337
12954
|
});
|
|
13338
12955
|
}
|
|
13339
|
-
this.statsReporter
|
|
13340
|
-
|
|
13341
|
-
|
|
13342
|
-
|
|
13343
|
-
|
|
13344
|
-
|
|
13345
|
-
|
|
13346
|
-
|
|
13347
|
-
|
|
12956
|
+
if (!this.statsReporter) {
|
|
12957
|
+
this.statsReporter = createStatsReporter({
|
|
12958
|
+
subscriber: this.subscriber,
|
|
12959
|
+
publisher: this.publisher,
|
|
12960
|
+
state: this.state,
|
|
12961
|
+
datacenter: this.sfuClient.edgeName,
|
|
12962
|
+
});
|
|
12963
|
+
}
|
|
12964
|
+
const clientDetails = getClientDetails();
|
|
12965
|
+
if (!this.sfuStatsReporter && statsOptions) {
|
|
13348
12966
|
this.sfuStatsReporter = new SfuStatsReporter(sfuClient, {
|
|
13349
12967
|
clientDetails,
|
|
13350
12968
|
options: statsOptions,
|
|
@@ -13353,258 +12971,129 @@ class Call {
|
|
|
13353
12971
|
});
|
|
13354
12972
|
this.sfuStatsReporter.start();
|
|
13355
12973
|
}
|
|
13356
|
-
};
|
|
13357
|
-
/**
|
|
13358
|
-
* Retrieves credentials for joining the call.
|
|
13359
|
-
*
|
|
13360
|
-
* @internal
|
|
13361
|
-
*
|
|
13362
|
-
* @param data the join call data.
|
|
13363
|
-
*/
|
|
13364
|
-
this.doJoinRequest = async (data) => {
|
|
13365
|
-
const location = await this.streamClient.getLocationHint();
|
|
13366
|
-
const request = { ...data, location };
|
|
13367
|
-
const joinResponse = await this.streamClient.post(`${this.streamClientBasePath}/join`, request);
|
|
13368
|
-
this.state.updateFromCallResponse(joinResponse.call);
|
|
13369
|
-
this.state.setMembers(joinResponse.members);
|
|
13370
|
-
this.state.setOwnCapabilities(joinResponse.own_capabilities);
|
|
13371
|
-
if (data?.ring && !this.ringing) {
|
|
13372
|
-
this.ringingSubject.next(true);
|
|
13373
|
-
}
|
|
13374
|
-
if (this.ringing && !this.isCreatedByMe) {
|
|
13375
|
-
// signals other users that I have accepted the incoming call.
|
|
13376
|
-
await this.accept();
|
|
13377
|
-
}
|
|
13378
|
-
if (this.streamClient._hasConnectionID()) {
|
|
13379
|
-
this.watching = true;
|
|
13380
|
-
this.clientStore.registerCall(this);
|
|
13381
|
-
}
|
|
13382
|
-
return joinResponse;
|
|
13383
|
-
};
|
|
13384
|
-
/**
|
|
13385
|
-
* Handles the reconnection flow.
|
|
13386
|
-
*
|
|
13387
|
-
* @internal
|
|
13388
|
-
*
|
|
13389
|
-
* @param strategy the reconnection strategy to use.
|
|
13390
|
-
*/
|
|
13391
|
-
this.reconnect = async (strategy) => {
|
|
13392
|
-
return withoutConcurrency(this.reconnectConcurrencyTag, async () => {
|
|
13393
|
-
this.logger('info', `[Reconnect] Reconnecting with strategy ${WebsocketReconnectStrategy[strategy]}`);
|
|
13394
|
-
this.reconnectStrategy = strategy;
|
|
13395
|
-
do {
|
|
13396
|
-
const current = WebsocketReconnectStrategy[this.reconnectStrategy];
|
|
13397
|
-
try {
|
|
13398
|
-
// wait until the network is available
|
|
13399
|
-
await this.networkAvailableTask?.promise;
|
|
13400
|
-
switch (this.reconnectStrategy) {
|
|
13401
|
-
case WebsocketReconnectStrategy.UNSPECIFIED:
|
|
13402
|
-
case WebsocketReconnectStrategy.DISCONNECT:
|
|
13403
|
-
this.logger('debug', `[Reconnect] No-op strategy ${current}`);
|
|
13404
|
-
break;
|
|
13405
|
-
case WebsocketReconnectStrategy.FAST:
|
|
13406
|
-
await this.reconnectFast();
|
|
13407
|
-
break;
|
|
13408
|
-
case WebsocketReconnectStrategy.REJOIN:
|
|
13409
|
-
await this.reconnectRejoin();
|
|
13410
|
-
break;
|
|
13411
|
-
case WebsocketReconnectStrategy.MIGRATE:
|
|
13412
|
-
await this.reconnectMigrate();
|
|
13413
|
-
break;
|
|
13414
|
-
default:
|
|
13415
|
-
ensureExhausted(this.reconnectStrategy, 'Unknown reconnection strategy');
|
|
13416
|
-
break;
|
|
13417
|
-
}
|
|
13418
|
-
break; // do-while loop, reconnection worked, exit the loop
|
|
13419
|
-
}
|
|
13420
|
-
catch (error) {
|
|
13421
|
-
this.logger('warn', `[Reconnect] ${current}(${this.reconnectAttempts}) failed. Attempting with REJOIN`, error);
|
|
13422
|
-
await sleep(retryInterval(this.reconnectAttempts));
|
|
13423
|
-
this.reconnectStrategy = WebsocketReconnectStrategy.REJOIN;
|
|
13424
|
-
this.reconnectAttempts++;
|
|
13425
|
-
}
|
|
13426
|
-
} while (this.state.callingState !== CallingState.JOINED &&
|
|
13427
|
-
this.state.callingState !== CallingState.LEFT);
|
|
13428
|
-
});
|
|
13429
|
-
};
|
|
13430
|
-
/**
|
|
13431
|
-
* Initiates the reconnection flow with the "fast" strategy.
|
|
13432
|
-
* @internal
|
|
13433
|
-
*/
|
|
13434
|
-
this.reconnectFast = async () => {
|
|
13435
|
-
this.reconnectStrategy = WebsocketReconnectStrategy.FAST;
|
|
13436
|
-
this.state.setCallingState(CallingState.RECONNECTING);
|
|
13437
|
-
return this.join(this.joinCallData);
|
|
13438
|
-
};
|
|
13439
|
-
/**
|
|
13440
|
-
* Initiates the reconnection flow with the "rejoin" strategy.
|
|
13441
|
-
* @internal
|
|
13442
|
-
*/
|
|
13443
|
-
this.reconnectRejoin = async () => {
|
|
13444
|
-
this.reconnectStrategy = WebsocketReconnectStrategy.REJOIN;
|
|
13445
|
-
this.state.setCallingState(CallingState.RECONNECTING);
|
|
13446
|
-
await this.join(this.joinCallData);
|
|
13447
|
-
await this.restorePublishedTracks();
|
|
13448
|
-
this.restoreSubscribedTracks();
|
|
13449
|
-
};
|
|
13450
|
-
/**
|
|
13451
|
-
* Initiates the reconnection flow with the "migrate" strategy.
|
|
13452
|
-
* @internal
|
|
13453
|
-
*/
|
|
13454
|
-
this.reconnectMigrate = async () => {
|
|
13455
|
-
const currentSfuClient = this.sfuClient;
|
|
13456
|
-
if (!currentSfuClient) {
|
|
13457
|
-
throw new Error('Cannot migrate without an active SFU client');
|
|
13458
|
-
}
|
|
13459
|
-
this.reconnectStrategy = WebsocketReconnectStrategy.MIGRATE;
|
|
13460
|
-
this.state.setCallingState(CallingState.MIGRATING);
|
|
13461
|
-
const currentSubscriber = this.subscriber;
|
|
13462
|
-
const currentPublisher = this.publisher;
|
|
13463
|
-
currentSubscriber?.detachEventHandlers();
|
|
13464
|
-
currentPublisher?.detachEventHandlers();
|
|
13465
|
-
const migrationTask = currentSfuClient.enterMigration();
|
|
13466
|
-
try {
|
|
13467
|
-
const currentSfu = currentSfuClient.edgeName;
|
|
13468
|
-
await this.join({ ...this.joinCallData, migrating_from: currentSfu });
|
|
13469
|
-
}
|
|
13470
|
-
finally {
|
|
13471
|
-
// cleanup the migration_from field after the migration is complete or failed
|
|
13472
|
-
// as we don't want to keep dirty data in the join call data
|
|
13473
|
-
delete this.joinCallData?.migrating_from;
|
|
13474
|
-
}
|
|
13475
|
-
await this.restorePublishedTracks();
|
|
13476
|
-
this.restoreSubscribedTracks();
|
|
13477
12974
|
try {
|
|
13478
|
-
//
|
|
13479
|
-
|
|
13480
|
-
|
|
13481
|
-
|
|
13482
|
-
|
|
13483
|
-
|
|
13484
|
-
|
|
13485
|
-
|
|
13486
|
-
|
|
13487
|
-
|
|
13488
|
-
|
|
13489
|
-
|
|
13490
|
-
|
|
13491
|
-
|
|
13492
|
-
|
|
13493
|
-
|
|
13494
|
-
|
|
13495
|
-
|
|
13496
|
-
|
|
13497
|
-
|
|
13498
|
-
|
|
13499
|
-
this.logger('warn', '[Reconnect] Error reconnecting', err);
|
|
13500
|
-
});
|
|
13501
|
-
});
|
|
13502
|
-
// handles the "error" event, through which the SFU can request a reconnect
|
|
13503
|
-
const unregisterOnError = this.on('error', (e) => {
|
|
13504
|
-
const { reconnectStrategy: strategy } = e;
|
|
13505
|
-
if (strategy === WebsocketReconnectStrategy.UNSPECIFIED)
|
|
13506
|
-
return;
|
|
13507
|
-
if (strategy === WebsocketReconnectStrategy.DISCONNECT) {
|
|
13508
|
-
this.leave({ reason: 'SFU instructed to disconnect' }).catch((err) => {
|
|
13509
|
-
this.logger('warn', `Can't leave call after disconnect request`, err);
|
|
12975
|
+
// 1. wait for the signal server to be ready before sending "joinRequest"
|
|
12976
|
+
sfuClient.signalReady
|
|
12977
|
+
.catch((err) => this.logger('error', 'Signal ready failed', err))
|
|
12978
|
+
// prepare a generic SDP and send it to the SFU.
|
|
12979
|
+
// this is a throw-away SDP that the SFU will use to determine
|
|
12980
|
+
// the capabilities of the client (codec support, etc.)
|
|
12981
|
+
.then(() => getGenericSdp('recvonly'))
|
|
12982
|
+
.then((sdp) => {
|
|
12983
|
+
const subscriptions = getCurrentValue(this.trackSubscriptionsSubject);
|
|
12984
|
+
const migration = isMigrating
|
|
12985
|
+
? {
|
|
12986
|
+
fromSfuId: data?.migrating_from || '',
|
|
12987
|
+
subscriptions: subscriptions.data || [],
|
|
12988
|
+
announcedTracks: this.publisher?.getCurrentTrackInfos() || [],
|
|
12989
|
+
}
|
|
12990
|
+
: undefined;
|
|
12991
|
+
return sfuClient.join({
|
|
12992
|
+
subscriberSdp: sdp || '',
|
|
12993
|
+
clientDetails,
|
|
12994
|
+
migration,
|
|
12995
|
+
fastReconnect: previousSfuClient?.isFastReconnecting ?? false,
|
|
13510
12996
|
});
|
|
12997
|
+
});
|
|
12998
|
+
// 2. in parallel, wait for the SFU to send us the "joinResponse"
|
|
12999
|
+
// this will throw an error if the SFU rejects the join request or
|
|
13000
|
+
// fails to respond in time
|
|
13001
|
+
const { callState, reconnected } = await this.waitForJoinResponse();
|
|
13002
|
+
if (isReconnecting) {
|
|
13003
|
+
this.logger('debug', '[Rejoin] fast reconnected:', reconnected);
|
|
13511
13004
|
}
|
|
13512
|
-
|
|
13513
|
-
this.
|
|
13514
|
-
|
|
13515
|
-
});
|
|
13005
|
+
if (isMigrating) {
|
|
13006
|
+
await this.subscriber.migrateTo(sfuClient, connectionConfig);
|
|
13007
|
+
await this.publisher?.migrateTo(sfuClient, connectionConfig);
|
|
13516
13008
|
}
|
|
13517
|
-
|
|
13518
|
-
|
|
13519
|
-
|
|
13520
|
-
|
|
13521
|
-
|
|
13522
|
-
|
|
13523
|
-
|
|
13524
|
-
|
|
13525
|
-
|
|
13526
|
-
networkAvailableTask.promise.then(() => {
|
|
13527
|
-
let strategy = WebsocketReconnectStrategy.FAST;
|
|
13528
|
-
if (this.lastOfflineTimestamp) {
|
|
13529
|
-
const offline = (Date.now() - this.lastOfflineTimestamp) / 1000;
|
|
13530
|
-
if (offline > this.fastReconnectDeadlineSeconds) {
|
|
13531
|
-
// We shouldn't attempt FAST if we have exceeded the deadline.
|
|
13532
|
-
// The SFU would have already wiped out the session.
|
|
13533
|
-
strategy = WebsocketReconnectStrategy.REJOIN;
|
|
13534
|
-
}
|
|
13009
|
+
else if (isReconnecting) {
|
|
13010
|
+
if (reconnected) {
|
|
13011
|
+
// update the SFU client instance on the subscriber and publisher
|
|
13012
|
+
this.subscriber.setSfuClient(sfuClient);
|
|
13013
|
+
// publisher might not be there (anonymous users)
|
|
13014
|
+
if (this.publisher) {
|
|
13015
|
+
this.publisher.setSfuClient(sfuClient);
|
|
13016
|
+
// and perform a full ICE restart on the publisher
|
|
13017
|
+
await this.publisher.restartIce();
|
|
13535
13018
|
}
|
|
13536
|
-
|
|
13537
|
-
|
|
13019
|
+
}
|
|
13020
|
+
else if (previousSfuClient?.isFastReconnecting) {
|
|
13021
|
+
// reconnection wasn't possible, so we need to do a full rejoin
|
|
13022
|
+
return await reconnect('full', 're-attempting').catch((err) => {
|
|
13023
|
+
this.logger('error', `[Rejoin]: Rejoin failed forced full rejoin.`, err);
|
|
13024
|
+
});
|
|
13025
|
+
}
|
|
13026
|
+
}
|
|
13027
|
+
const currentParticipants = callState?.participants || [];
|
|
13028
|
+
const participantCount = callState?.participantCount;
|
|
13029
|
+
const startedAt = callState?.startedAt
|
|
13030
|
+
? Timestamp.toDate(callState.startedAt)
|
|
13031
|
+
: new Date();
|
|
13032
|
+
const pins = callState?.pins ?? [];
|
|
13033
|
+
this.state.setParticipants(() => {
|
|
13034
|
+
const participantLookup = this.state.getParticipantLookupBySessionId();
|
|
13035
|
+
return currentParticipants.map((p) => {
|
|
13036
|
+
// We need to preserve the local state of the participant
|
|
13037
|
+
// (e.g. videoDimension, visibilityState, pinnedAt, etc.)
|
|
13038
|
+
// as it doesn't exist on the server.
|
|
13039
|
+
const existingParticipant = participantLookup[p.sessionId];
|
|
13040
|
+
return Object.assign(p, existingParticipant, {
|
|
13041
|
+
isLocalParticipant: p.sessionId === sfuClient.sessionId,
|
|
13042
|
+
viewportVisibilityState: existingParticipant?.viewportVisibilityState ?? {
|
|
13043
|
+
videoTrack: VisibilityState.UNKNOWN,
|
|
13044
|
+
screenShareTrack: VisibilityState.UNKNOWN,
|
|
13045
|
+
},
|
|
13538
13046
|
});
|
|
13539
13047
|
});
|
|
13540
|
-
|
|
13541
|
-
|
|
13542
|
-
|
|
13048
|
+
});
|
|
13049
|
+
this.state.setParticipantCount(participantCount?.total || 0);
|
|
13050
|
+
this.state.setAnonymousParticipantCount(participantCount?.anonymous || 0);
|
|
13051
|
+
this.state.setStartedAt(startedAt);
|
|
13052
|
+
this.state.setServerSidePins(pins);
|
|
13053
|
+
this.reconnectAttempts = 0; // reset the reconnect attempts counter
|
|
13054
|
+
this.state.setCallingState(CallingState.JOINED);
|
|
13055
|
+
try {
|
|
13056
|
+
await this.initCamera({ setStatus: true });
|
|
13057
|
+
await this.initMic({ setStatus: true });
|
|
13543
13058
|
}
|
|
13544
|
-
|
|
13545
|
-
this.logger('
|
|
13546
|
-
// TODO try to remove this .close call
|
|
13547
|
-
this.sfuClient?.close(4002, 'Closing WS to reconnect after going online');
|
|
13548
|
-
// we went online, release the previous waiters and reset the state
|
|
13549
|
-
this.networkAvailableTask?.resolve();
|
|
13550
|
-
this.networkAvailableTask = undefined;
|
|
13551
|
-
this.sfuStatsReporter?.start();
|
|
13059
|
+
catch (error) {
|
|
13060
|
+
this.logger('warn', 'Camera and/or mic init failed during join call', error);
|
|
13552
13061
|
}
|
|
13553
|
-
|
|
13554
|
-
|
|
13555
|
-
|
|
13556
|
-
|
|
13557
|
-
|
|
13558
|
-
|
|
13559
|
-
|
|
13560
|
-
|
|
13561
|
-
|
|
13562
|
-
|
|
13563
|
-
|
|
13564
|
-
|
|
13565
|
-
|
|
13566
|
-
|
|
13567
|
-
|
|
13568
|
-
|
|
13569
|
-
|
|
13570
|
-
|
|
13571
|
-
|
|
13572
|
-
|
|
13573
|
-
case TrackType.VIDEO:
|
|
13574
|
-
const videoStream = this.camera.state.mediaStream;
|
|
13575
|
-
if (videoStream) {
|
|
13576
|
-
await this.publishVideoStream(videoStream, {
|
|
13577
|
-
preferredCodec: this.camera.preferredCodec,
|
|
13578
|
-
});
|
|
13579
|
-
}
|
|
13580
|
-
break;
|
|
13581
|
-
case TrackType.SCREEN_SHARE:
|
|
13582
|
-
const screenShareStream = this.screenShare.state.mediaStream;
|
|
13583
|
-
if (screenShareStream) {
|
|
13584
|
-
await this.publishScreenShareStream(screenShareStream, {
|
|
13585
|
-
screenShareSettings: this.screenShare.getSettings(),
|
|
13586
|
-
});
|
|
13587
|
-
}
|
|
13588
|
-
break;
|
|
13589
|
-
// screen share audio can't exist without a screen share, so we handle it there
|
|
13590
|
-
case TrackType.SCREEN_SHARE_AUDIO:
|
|
13591
|
-
case TrackType.UNSPECIFIED:
|
|
13592
|
-
break;
|
|
13593
|
-
default:
|
|
13594
|
-
ensureExhausted(trackType, 'Unknown track type');
|
|
13595
|
-
break;
|
|
13062
|
+
// 3. once we have the "joinResponse", and possibly reconciled the local state
|
|
13063
|
+
// we schedule a fast subscription update for all remote participants
|
|
13064
|
+
// that were visible before we reconnected or migrated to a new SFU.
|
|
13065
|
+
const { remoteParticipants } = this.state;
|
|
13066
|
+
if (remoteParticipants.length > 0) {
|
|
13067
|
+
this.updateSubscriptions(remoteParticipants, DebounceType.FAST);
|
|
13068
|
+
}
|
|
13069
|
+
this.logger('info', `Joined call ${this.cid}`);
|
|
13070
|
+
}
|
|
13071
|
+
catch (err) {
|
|
13072
|
+
// join failed, try to rejoin
|
|
13073
|
+
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
13074
|
+
this.logger('error', `[Rejoin]: Rejoin ${this.reconnectAttempts} failed.`, err);
|
|
13075
|
+
await reconnect('full', 'previous attempt failed');
|
|
13076
|
+
this.logger('info', `[Rejoin]: Rejoin ${this.reconnectAttempts} successful!`);
|
|
13077
|
+
}
|
|
13078
|
+
else {
|
|
13079
|
+
this.logger('error', `[Rejoin]: Rejoin failed for ${this.reconnectAttempts} times. Giving up.`);
|
|
13080
|
+
this.state.setCallingState(CallingState.RECONNECTING_FAILED);
|
|
13081
|
+
throw new Error('Join failed');
|
|
13596
13082
|
}
|
|
13597
13083
|
}
|
|
13598
13084
|
};
|
|
13599
|
-
|
|
13600
|
-
|
|
13601
|
-
|
|
13602
|
-
|
|
13603
|
-
|
|
13604
|
-
|
|
13605
|
-
|
|
13606
|
-
|
|
13607
|
-
|
|
13085
|
+
this.waitForJoinResponse = (timeout = 5000) => {
|
|
13086
|
+
return new Promise((resolve, reject) => {
|
|
13087
|
+
const unsubscribe = this.on('joinResponse', (event) => {
|
|
13088
|
+
clearTimeout(timeoutId);
|
|
13089
|
+
unsubscribe();
|
|
13090
|
+
resolve(event);
|
|
13091
|
+
});
|
|
13092
|
+
const timeoutId = setTimeout(() => {
|
|
13093
|
+
unsubscribe();
|
|
13094
|
+
reject(new Error('Waiting for "joinResponse" has timed out'));
|
|
13095
|
+
}, timeout);
|
|
13096
|
+
});
|
|
13608
13097
|
};
|
|
13609
13098
|
/**
|
|
13610
13099
|
* Starts publishing the given video stream to the call.
|
|
@@ -13619,7 +13108,7 @@ class Call {
|
|
|
13619
13108
|
this.publishVideoStream = async (videoStream, opts = {}) => {
|
|
13620
13109
|
// we should wait until we get a JoinResponse from the SFU,
|
|
13621
13110
|
// otherwise we risk breaking the ICETrickle flow.
|
|
13622
|
-
await this.
|
|
13111
|
+
await this.assertCallJoined();
|
|
13623
13112
|
if (!this.publisher) {
|
|
13624
13113
|
this.logger('error', 'Trying to publish video before join is completed');
|
|
13625
13114
|
throw new Error(`Call not joined yet.`);
|
|
@@ -13629,9 +13118,6 @@ class Call {
|
|
|
13629
13118
|
this.logger('error', `There is no video track to publish in the stream.`);
|
|
13630
13119
|
return;
|
|
13631
13120
|
}
|
|
13632
|
-
if (!this.trackPublishOrder.includes(TrackType.VIDEO)) {
|
|
13633
|
-
this.trackPublishOrder.push(TrackType.VIDEO);
|
|
13634
|
-
}
|
|
13635
13121
|
await this.publisher.publishStream(videoStream, videoTrack, TrackType.VIDEO, opts);
|
|
13636
13122
|
};
|
|
13637
13123
|
/**
|
|
@@ -13646,7 +13132,7 @@ class Call {
|
|
|
13646
13132
|
this.publishAudioStream = async (audioStream) => {
|
|
13647
13133
|
// we should wait until we get a JoinResponse from the SFU,
|
|
13648
13134
|
// otherwise we risk breaking the ICETrickle flow.
|
|
13649
|
-
await this.
|
|
13135
|
+
await this.assertCallJoined();
|
|
13650
13136
|
if (!this.publisher) {
|
|
13651
13137
|
this.logger('error', 'Trying to publish audio before join is completed');
|
|
13652
13138
|
throw new Error(`Call not joined yet.`);
|
|
@@ -13656,9 +13142,6 @@ class Call {
|
|
|
13656
13142
|
this.logger('error', `There is no audio track in the stream to publish`);
|
|
13657
13143
|
return;
|
|
13658
13144
|
}
|
|
13659
|
-
if (!this.trackPublishOrder.includes(TrackType.AUDIO)) {
|
|
13660
|
-
this.trackPublishOrder.push(TrackType.AUDIO);
|
|
13661
|
-
}
|
|
13662
13145
|
await this.publisher.publishStream(audioStream, audioTrack, TrackType.AUDIO);
|
|
13663
13146
|
};
|
|
13664
13147
|
/**
|
|
@@ -13673,7 +13156,7 @@ class Call {
|
|
|
13673
13156
|
this.publishScreenShareStream = async (screenShareStream, opts = {}) => {
|
|
13674
13157
|
// we should wait until we get a JoinResponse from the SFU,
|
|
13675
13158
|
// otherwise we risk breaking the ICETrickle flow.
|
|
13676
|
-
await this.
|
|
13159
|
+
await this.assertCallJoined();
|
|
13677
13160
|
if (!this.publisher) {
|
|
13678
13161
|
this.logger('error', 'Trying to publish screen share before join is completed');
|
|
13679
13162
|
throw new Error(`Call not joined yet.`);
|
|
@@ -13683,15 +13166,9 @@ class Call {
|
|
|
13683
13166
|
this.logger('error', `There is no video track in the screen share stream to publish`);
|
|
13684
13167
|
return;
|
|
13685
13168
|
}
|
|
13686
|
-
if (!this.trackPublishOrder.includes(TrackType.SCREEN_SHARE)) {
|
|
13687
|
-
this.trackPublishOrder.push(TrackType.SCREEN_SHARE);
|
|
13688
|
-
}
|
|
13689
13169
|
await this.publisher.publishStream(screenShareStream, screenShareTrack, TrackType.SCREEN_SHARE, opts);
|
|
13690
13170
|
const [screenShareAudioTrack] = screenShareStream.getAudioTracks();
|
|
13691
13171
|
if (screenShareAudioTrack) {
|
|
13692
|
-
if (!this.trackPublishOrder.includes(TrackType.SCREEN_SHARE_AUDIO)) {
|
|
13693
|
-
this.trackPublishOrder.push(TrackType.SCREEN_SHARE_AUDIO);
|
|
13694
|
-
}
|
|
13695
13172
|
await this.publisher.publishStream(screenShareStream, screenShareAudioTrack, TrackType.SCREEN_SHARE_AUDIO, opts);
|
|
13696
13173
|
}
|
|
13697
13174
|
};
|
|
@@ -13736,9 +13213,19 @@ class Call {
|
|
|
13736
13213
|
* @param type the debounce type to use for the update.
|
|
13737
13214
|
*/
|
|
13738
13215
|
this.updateSubscriptionsPartial = (trackType, changes, type = DebounceType.SLOW) => {
|
|
13216
|
+
if (trackType === 'video') {
|
|
13217
|
+
this.logger('warn', `updateSubscriptionsPartial: ${trackType} is deprecated. Please switch to 'videoTrack'`);
|
|
13218
|
+
trackType = 'videoTrack';
|
|
13219
|
+
}
|
|
13220
|
+
else if (trackType === 'screen') {
|
|
13221
|
+
this.logger('warn', `updateSubscriptionsPartial: ${trackType} is deprecated. Please switch to 'screenShareTrack'`);
|
|
13222
|
+
trackType = 'screenShareTrack';
|
|
13223
|
+
}
|
|
13739
13224
|
const participants = this.state.updateParticipants(Object.entries(changes).reduce((acc, [sessionId, change]) => {
|
|
13740
|
-
if (change.dimension) {
|
|
13225
|
+
if (change.dimension?.height) {
|
|
13741
13226
|
change.dimension.height = Math.ceil(change.dimension.height);
|
|
13227
|
+
}
|
|
13228
|
+
if (change.dimension?.width) {
|
|
13742
13229
|
change.dimension.width = Math.ceil(change.dimension.width);
|
|
13743
13230
|
}
|
|
13744
13231
|
const prop = trackType === 'videoTrack'
|
|
@@ -13753,7 +13240,9 @@ class Call {
|
|
|
13753
13240
|
}
|
|
13754
13241
|
return acc;
|
|
13755
13242
|
}, {}));
|
|
13756
|
-
|
|
13243
|
+
if (participants) {
|
|
13244
|
+
this.updateSubscriptions(participants, type);
|
|
13245
|
+
}
|
|
13757
13246
|
};
|
|
13758
13247
|
this.updateSubscriptions = (participants, type = DebounceType.SLOW) => {
|
|
13759
13248
|
const subscriptions = [];
|
|
@@ -13836,10 +13325,10 @@ class Call {
|
|
|
13836
13325
|
this.updatePublishQuality = async (enabledLayers) => {
|
|
13837
13326
|
return this.publisher?.updateVideoPublishQuality(enabledLayers);
|
|
13838
13327
|
};
|
|
13839
|
-
this.
|
|
13328
|
+
this.assertCallJoined = () => {
|
|
13840
13329
|
return new Promise((resolve) => {
|
|
13841
13330
|
this.state.callingState$
|
|
13842
|
-
.pipe(takeWhile((state) => state !== CallingState.JOINED, true), filter((
|
|
13331
|
+
.pipe(takeWhile((state) => state !== CallingState.JOINED, true), filter((s) => s === CallingState.JOINED))
|
|
13843
13332
|
.subscribe(() => resolve());
|
|
13844
13333
|
});
|
|
13845
13334
|
};
|
|
@@ -14223,72 +13712,14 @@ class Call {
|
|
|
14223
13712
|
*
|
|
14224
13713
|
* @internal
|
|
14225
13714
|
*/
|
|
14226
|
-
this.applyDeviceConfig = async (
|
|
14227
|
-
await this.initCamera({ setStatus:
|
|
13715
|
+
this.applyDeviceConfig = async () => {
|
|
13716
|
+
await this.initCamera({ setStatus: false }).catch((err) => {
|
|
14228
13717
|
this.logger('warn', 'Camera init failed', err);
|
|
14229
13718
|
});
|
|
14230
|
-
await this.initMic({ setStatus:
|
|
13719
|
+
await this.initMic({ setStatus: false }).catch((err) => {
|
|
14231
13720
|
this.logger('warn', 'Mic init failed', err);
|
|
14232
13721
|
});
|
|
14233
13722
|
};
|
|
14234
|
-
this.initCamera = async (options) => {
|
|
14235
|
-
// Wait for any in progress camera operation
|
|
14236
|
-
await this.camera.statusChangeSettled();
|
|
14237
|
-
if (this.state.localParticipant?.videoStream ||
|
|
14238
|
-
!this.permissionsContext.hasPermission('send-video')) {
|
|
14239
|
-
return;
|
|
14240
|
-
}
|
|
14241
|
-
// Set camera direction if it's not yet set
|
|
14242
|
-
if (!this.camera.state.direction && !this.camera.state.selectedDevice) {
|
|
14243
|
-
let defaultDirection = 'front';
|
|
14244
|
-
const backendSetting = this.state.settings?.video.camera_facing;
|
|
14245
|
-
if (backendSetting) {
|
|
14246
|
-
defaultDirection = backendSetting === 'front' ? 'front' : 'back';
|
|
14247
|
-
}
|
|
14248
|
-
this.camera.state.setDirection(defaultDirection);
|
|
14249
|
-
}
|
|
14250
|
-
// Set target resolution
|
|
14251
|
-
const targetResolution = this.state.settings?.video.target_resolution;
|
|
14252
|
-
if (targetResolution) {
|
|
14253
|
-
await this.camera.selectTargetResolution(targetResolution);
|
|
14254
|
-
}
|
|
14255
|
-
if (options.setStatus) {
|
|
14256
|
-
// Publish already that was set before we joined
|
|
14257
|
-
if (this.camera.enabled &&
|
|
14258
|
-
this.camera.state.mediaStream &&
|
|
14259
|
-
!this.publisher?.isPublishing(TrackType.VIDEO)) {
|
|
14260
|
-
await this.publishVideoStream(this.camera.state.mediaStream, {
|
|
14261
|
-
preferredCodec: this.camera.preferredCodec,
|
|
14262
|
-
});
|
|
14263
|
-
}
|
|
14264
|
-
// Start camera if backend config specifies, and there is no local setting
|
|
14265
|
-
if (this.camera.state.status === undefined &&
|
|
14266
|
-
this.state.settings?.video.camera_default_on) {
|
|
14267
|
-
await this.camera.enable();
|
|
14268
|
-
}
|
|
14269
|
-
}
|
|
14270
|
-
};
|
|
14271
|
-
this.initMic = async (options) => {
|
|
14272
|
-
// Wait for any in progress mic operation
|
|
14273
|
-
await this.microphone.statusChangeSettled();
|
|
14274
|
-
if (this.state.localParticipant?.audioStream ||
|
|
14275
|
-
!this.permissionsContext.hasPermission('send-audio')) {
|
|
14276
|
-
return;
|
|
14277
|
-
}
|
|
14278
|
-
if (options.setStatus) {
|
|
14279
|
-
// Publish media stream that was set before we joined
|
|
14280
|
-
if (this.microphone.enabled &&
|
|
14281
|
-
this.microphone.state.mediaStream &&
|
|
14282
|
-
!this.publisher?.isPublishing(TrackType.AUDIO)) {
|
|
14283
|
-
await this.publishAudioStream(this.microphone.state.mediaStream);
|
|
14284
|
-
}
|
|
14285
|
-
// Start mic if backend config specifies, and there is no local setting
|
|
14286
|
-
if (this.microphone.state.status === undefined &&
|
|
14287
|
-
this.state.settings?.audio.mic_default_on) {
|
|
14288
|
-
await this.microphone.enable();
|
|
14289
|
-
}
|
|
14290
|
-
}
|
|
14291
|
-
};
|
|
14292
13723
|
/**
|
|
14293
13724
|
* Will begin tracking the given element for visibility changes within the
|
|
14294
13725
|
* configured viewport element (`call.setViewport`).
|
|
@@ -14403,15 +13834,15 @@ class Call {
|
|
|
14403
13834
|
}
|
|
14404
13835
|
async setup() {
|
|
14405
13836
|
await withoutConcurrency(this.joinLeaveConcurrencyTag, async () => {
|
|
14406
|
-
if (this.initialized)
|
|
13837
|
+
if (this.initialized) {
|
|
14407
13838
|
return;
|
|
13839
|
+
}
|
|
14408
13840
|
this.leaveCallHooks.add(this.on('all', (event) => {
|
|
14409
13841
|
// update state with the latest event data
|
|
14410
13842
|
this.state.updateFromEvent(event);
|
|
14411
13843
|
}));
|
|
14412
|
-
this.leaveCallHooks.add(registerEventHandlers(this, this.dispatcher));
|
|
13844
|
+
this.leaveCallHooks.add(registerEventHandlers(this, this.state, this.dispatcher));
|
|
14413
13845
|
this.registerEffects();
|
|
14414
|
-
this.registerReconnectHandlers();
|
|
14415
13846
|
this.leaveCallHooks.add(createSubscription(this.trackSubscriptionsSubject.pipe(debounce((v) => timer(v.type)), map$1((v) => v.data)), (subscriptions) => this.sfuClient?.updateSubscriptions(subscriptions).catch((err) => {
|
|
14416
13847
|
this.logger('debug', `Failed to update track subscriptions`, err);
|
|
14417
13848
|
})));
|
|
@@ -14431,7 +13862,44 @@ class Call {
|
|
|
14431
13862
|
}));
|
|
14432
13863
|
this.leaveCallHooks.add(
|
|
14433
13864
|
// handle the case when the user permissions are modified.
|
|
14434
|
-
|
|
13865
|
+
createSubscription(this.state.ownCapabilities$, (ownCapabilities) => {
|
|
13866
|
+
// update the permission context.
|
|
13867
|
+
this.permissionsContext.setPermissions(ownCapabilities);
|
|
13868
|
+
if (!this.publisher)
|
|
13869
|
+
return;
|
|
13870
|
+
// check if the user still has publishing permissions and stop publishing if not.
|
|
13871
|
+
const permissionToTrackType = {
|
|
13872
|
+
[OwnCapability.SEND_AUDIO]: TrackType.AUDIO,
|
|
13873
|
+
[OwnCapability.SEND_VIDEO]: TrackType.VIDEO,
|
|
13874
|
+
[OwnCapability.SCREENSHARE]: TrackType.SCREEN_SHARE,
|
|
13875
|
+
};
|
|
13876
|
+
for (const [permission, trackType] of Object.entries(permissionToTrackType)) {
|
|
13877
|
+
const hasPermission = this.permissionsContext.hasPermission(permission);
|
|
13878
|
+
if (!hasPermission &&
|
|
13879
|
+
(this.publisher.isPublishing(trackType) ||
|
|
13880
|
+
this.publisher.isLive(trackType))) {
|
|
13881
|
+
// Stop tracks, then notify device manager
|
|
13882
|
+
this.stopPublish(trackType)
|
|
13883
|
+
.catch((err) => {
|
|
13884
|
+
this.logger('error', `Error stopping publish ${trackType}`, err);
|
|
13885
|
+
})
|
|
13886
|
+
.then(() => {
|
|
13887
|
+
if (trackType === TrackType.VIDEO &&
|
|
13888
|
+
this.camera.state.status === 'enabled') {
|
|
13889
|
+
this.camera
|
|
13890
|
+
.disable()
|
|
13891
|
+
.catch((err) => this.logger('error', `Error disabling camera after permission revoked`, err));
|
|
13892
|
+
}
|
|
13893
|
+
if (trackType === TrackType.AUDIO &&
|
|
13894
|
+
this.microphone.state.status === 'enabled') {
|
|
13895
|
+
this.microphone
|
|
13896
|
+
.disable()
|
|
13897
|
+
.catch((err) => this.logger('error', `Error disabling microphone after permission revoked`, err));
|
|
13898
|
+
}
|
|
13899
|
+
});
|
|
13900
|
+
}
|
|
13901
|
+
}
|
|
13902
|
+
}));
|
|
14435
13903
|
this.leaveCallHooks.add(
|
|
14436
13904
|
// handles the case when the user is blocked by the call owner.
|
|
14437
13905
|
createSubscription(this.state.blockedUserIds$, async (blockedUserIds) => {
|
|
@@ -14523,20 +13991,63 @@ class Call {
|
|
|
14523
13991
|
get isCreatedByMe() {
|
|
14524
13992
|
return this.state.createdBy?.id === this.currentUserId;
|
|
14525
13993
|
}
|
|
14526
|
-
|
|
14527
|
-
|
|
14528
|
-
|
|
14529
|
-
|
|
14530
|
-
|
|
14531
|
-
*/
|
|
14532
|
-
handleSfuSignalClose(sfuClient) {
|
|
14533
|
-
this.logger('debug', '[Reconnect] SFU signal connection closed');
|
|
14534
|
-
// normal close, no need to reconnect
|
|
14535
|
-
if (sfuClient.isLeaving)
|
|
13994
|
+
async initCamera(options) {
|
|
13995
|
+
// Wait for any in progress camera operation
|
|
13996
|
+
await this.camera.statusChangeSettled();
|
|
13997
|
+
if (this.state.localParticipant?.videoStream ||
|
|
13998
|
+
!this.permissionsContext.hasPermission('send-video')) {
|
|
14536
13999
|
return;
|
|
14537
|
-
|
|
14538
|
-
|
|
14539
|
-
|
|
14000
|
+
}
|
|
14001
|
+
// Set camera direction if it's not yet set
|
|
14002
|
+
if (!this.camera.state.direction && !this.camera.state.selectedDevice) {
|
|
14003
|
+
let defaultDirection = 'front';
|
|
14004
|
+
const backendSetting = this.state.settings?.video.camera_facing;
|
|
14005
|
+
if (backendSetting) {
|
|
14006
|
+
defaultDirection = backendSetting === 'front' ? 'front' : 'back';
|
|
14007
|
+
}
|
|
14008
|
+
this.camera.state.setDirection(defaultDirection);
|
|
14009
|
+
}
|
|
14010
|
+
// Set target resolution
|
|
14011
|
+
const targetResolution = this.state.settings?.video.target_resolution;
|
|
14012
|
+
if (targetResolution) {
|
|
14013
|
+
await this.camera.selectTargetResolution(targetResolution);
|
|
14014
|
+
}
|
|
14015
|
+
if (options.setStatus) {
|
|
14016
|
+
// Publish already that was set before we joined
|
|
14017
|
+
if (this.camera.state.status === 'enabled' &&
|
|
14018
|
+
this.camera.state.mediaStream &&
|
|
14019
|
+
!this.publisher?.isPublishing(TrackType.VIDEO)) {
|
|
14020
|
+
await this.publishVideoStream(this.camera.state.mediaStream, {
|
|
14021
|
+
preferredCodec: this.camera.preferredCodec,
|
|
14022
|
+
});
|
|
14023
|
+
}
|
|
14024
|
+
// Start camera if backend config specifies, and there is no local setting
|
|
14025
|
+
if (this.camera.state.status === undefined &&
|
|
14026
|
+
this.state.settings?.video.camera_default_on) {
|
|
14027
|
+
await this.camera.enable();
|
|
14028
|
+
}
|
|
14029
|
+
}
|
|
14030
|
+
}
|
|
14031
|
+
async initMic(options) {
|
|
14032
|
+
// Wait for any in progress mic operation
|
|
14033
|
+
await this.microphone.statusChangeSettled();
|
|
14034
|
+
if (this.state.localParticipant?.audioStream ||
|
|
14035
|
+
!this.permissionsContext.hasPermission('send-audio')) {
|
|
14036
|
+
return;
|
|
14037
|
+
}
|
|
14038
|
+
if (options.setStatus) {
|
|
14039
|
+
// Publish media stream that was set before we joined
|
|
14040
|
+
if (this.microphone.state.status === 'enabled' &&
|
|
14041
|
+
this.microphone.state.mediaStream &&
|
|
14042
|
+
!this.publisher?.isPublishing(TrackType.AUDIO)) {
|
|
14043
|
+
await this.publishAudioStream(this.microphone.state.mediaStream);
|
|
14044
|
+
}
|
|
14045
|
+
// Start mic if backend config specifies, and there is no local setting
|
|
14046
|
+
if (this.microphone.state.status === undefined &&
|
|
14047
|
+
this.state.settings?.audio.mic_default_on) {
|
|
14048
|
+
await this.microphone.enable();
|
|
14049
|
+
}
|
|
14050
|
+
}
|
|
14540
14051
|
}
|
|
14541
14052
|
}
|
|
14542
14053
|
|
|
@@ -14739,7 +14250,6 @@ class StableWSConnection {
|
|
|
14739
14250
|
}
|
|
14740
14251
|
}
|
|
14741
14252
|
if (data) {
|
|
14742
|
-
data.received_at = new Date();
|
|
14743
14253
|
this.client.dispatchEvent(data);
|
|
14744
14254
|
}
|
|
14745
14255
|
this.scheduleConnectionCheck();
|
|
@@ -15080,7 +14590,7 @@ class StableWSConnection {
|
|
|
15080
14590
|
wsURL,
|
|
15081
14591
|
requestID: this.requestID,
|
|
15082
14592
|
});
|
|
15083
|
-
this.ws = new WebSocket
|
|
14593
|
+
this.ws = new WebSocket(wsURL);
|
|
15084
14594
|
this.ws.onopen = this.onopen.bind(this, this.wsID);
|
|
15085
14595
|
this.ws.onclose = this.onclose.bind(this, this.wsID);
|
|
15086
14596
|
this.ws.onerror = this.onerror.bind(this, this.wsID);
|
|
@@ -15366,7 +14876,7 @@ class TokenManager {
|
|
|
15366
14876
|
if (this.user && !this.token) {
|
|
15367
14877
|
return this.token;
|
|
15368
14878
|
}
|
|
15369
|
-
throw new Error(`
|
|
14879
|
+
throw new Error(`User token is not set. Either client.connectUser wasn't called or client.disconnect was called`);
|
|
15370
14880
|
};
|
|
15371
14881
|
this.isStatic = () => this.type === 'static';
|
|
15372
14882
|
this.loadTokenPromise = null;
|
|
@@ -15696,7 +15206,6 @@ class StreamClient {
|
|
|
15696
15206
|
const wsPromise = this.openConnection();
|
|
15697
15207
|
this.setUserPromise = Promise.all([setTokenPromise, wsPromise]).then((result) => result[1]);
|
|
15698
15208
|
try {
|
|
15699
|
-
addConnectionEventListeners(this.updateNetworkConnectionStatus);
|
|
15700
15209
|
return await this.setUserPromise;
|
|
15701
15210
|
}
|
|
15702
15211
|
catch (err) {
|
|
@@ -15792,7 +15301,6 @@ class StreamClient {
|
|
|
15792
15301
|
delete this.userID;
|
|
15793
15302
|
this.anonymous = false;
|
|
15794
15303
|
await this.closeConnection(timeout);
|
|
15795
|
-
removeConnectionEventListeners(this.updateNetworkConnectionStatus);
|
|
15796
15304
|
this.tokenManager.reset();
|
|
15797
15305
|
this.connectionIdPromise = undefined;
|
|
15798
15306
|
this.rejectConnectionId = undefined;
|
|
@@ -15812,7 +15320,6 @@ class StreamClient {
|
|
|
15812
15320
|
* connectAnonymousUser - Set an anonymous user and open a WebSocket connection
|
|
15813
15321
|
*/
|
|
15814
15322
|
this.connectAnonymousUser = async (user, tokenOrProvider) => {
|
|
15815
|
-
addConnectionEventListeners(this.updateNetworkConnectionStatus);
|
|
15816
15323
|
this.connectionIdPromise = new Promise((resolve, reject) => {
|
|
15817
15324
|
this.resolveConnectionId = resolve;
|
|
15818
15325
|
this.rejectConnectionId = reject;
|
|
@@ -15972,6 +15479,8 @@ class StreamClient {
|
|
|
15972
15479
|
return data;
|
|
15973
15480
|
};
|
|
15974
15481
|
this.dispatchEvent = (event) => {
|
|
15482
|
+
if (!event.received_at)
|
|
15483
|
+
event.received_at = new Date();
|
|
15975
15484
|
this.logger('debug', `Dispatching event: ${event.type}`, event);
|
|
15976
15485
|
if (!this.listeners)
|
|
15977
15486
|
return;
|
|
@@ -16062,7 +15571,7 @@ class StreamClient {
|
|
|
16062
15571
|
});
|
|
16063
15572
|
};
|
|
16064
15573
|
this.getUserAgent = () => {
|
|
16065
|
-
const version = "1.5.0
|
|
15574
|
+
const version = "1.5.0" ;
|
|
16066
15575
|
return (this.userAgent ||
|
|
16067
15576
|
`stream-video-javascript-client-${this.node ? 'node' : 'browser'}-${version}`);
|
|
16068
15577
|
};
|
|
@@ -16128,15 +15637,11 @@ class StreamClient {
|
|
|
16128
15637
|
client_request_id,
|
|
16129
15638
|
});
|
|
16130
15639
|
};
|
|
16131
|
-
|
|
16132
|
-
|
|
16133
|
-
|
|
16134
|
-
|
|
16135
|
-
|
|
16136
|
-
else if (event.type === 'online') {
|
|
16137
|
-
this.logger('debug', 'device went online');
|
|
16138
|
-
this.dispatchEvent({ type: 'network.changed', online: true });
|
|
16139
|
-
}
|
|
15640
|
+
/**
|
|
15641
|
+
* creates an abort controller that will be used by the next HTTP Request.
|
|
15642
|
+
*/
|
|
15643
|
+
this.createAbortControllerForNextRequest = () => {
|
|
15644
|
+
return (this.nextRequestAbortController = new AbortController());
|
|
16140
15645
|
};
|
|
16141
15646
|
// set the key
|
|
16142
15647
|
this.key = key;
|
|
@@ -16166,14 +15671,10 @@ class StreamClient {
|
|
|
16166
15671
|
});
|
|
16167
15672
|
}
|
|
16168
15673
|
this.setBaseURL(this.options.baseURL || 'https://video.stream-io-api.com/video');
|
|
16169
|
-
if (typeof process !== 'undefined' &&
|
|
16170
|
-
'env' in process &&
|
|
16171
|
-
process.env.STREAM_LOCAL_TEST_RUN) {
|
|
15674
|
+
if (typeof process !== 'undefined' && process.env.STREAM_LOCAL_TEST_RUN) {
|
|
16172
15675
|
this.setBaseURL('http://localhost:3030/video');
|
|
16173
15676
|
}
|
|
16174
|
-
if (typeof process !== 'undefined' &&
|
|
16175
|
-
'env' in process &&
|
|
16176
|
-
process.env.STREAM_LOCAL_TEST_HOST) {
|
|
15677
|
+
if (typeof process !== 'undefined' && process.env.STREAM_LOCAL_TEST_HOST) {
|
|
16177
15678
|
this.setBaseURL(`http://${process.env.STREAM_LOCAL_TEST_HOST}/video`);
|
|
16178
15679
|
}
|
|
16179
15680
|
this.axiosInstance = axios.create({
|
|
@@ -16208,99 +15709,6 @@ class StreamVideoClient {
|
|
|
16208
15709
|
constructor(apiKeyOrArgs, opts) {
|
|
16209
15710
|
this.logLevel = 'warn';
|
|
16210
15711
|
this.eventHandlersToUnregister = [];
|
|
16211
|
-
/**
|
|
16212
|
-
* Connects the given user to the client.
|
|
16213
|
-
* Only one user can connect at a time, if you want to change users, call `disconnectUser` before connecting a new user.
|
|
16214
|
-
* If the connection is successful, the connected user [state variable](#readonlystatestore) will be updated accordingly.
|
|
16215
|
-
*
|
|
16216
|
-
* @param user the user to connect.
|
|
16217
|
-
* @param token a token or a function that returns a token.
|
|
16218
|
-
*/
|
|
16219
|
-
this.connectUser = async (user, token) => {
|
|
16220
|
-
if (user.type === 'anonymous') {
|
|
16221
|
-
user.id = '!anon';
|
|
16222
|
-
return this.connectAnonymousUser(user, token);
|
|
16223
|
-
}
|
|
16224
|
-
let connectUser = () => {
|
|
16225
|
-
return this.streamClient.connectUser(user, token);
|
|
16226
|
-
};
|
|
16227
|
-
if (user.type === 'guest') {
|
|
16228
|
-
connectUser = async () => {
|
|
16229
|
-
return this.streamClient.connectGuestUser(user);
|
|
16230
|
-
};
|
|
16231
|
-
}
|
|
16232
|
-
this.connectionPromise = this.disconnectionPromise
|
|
16233
|
-
? this.disconnectionPromise.then(() => connectUser())
|
|
16234
|
-
: connectUser();
|
|
16235
|
-
this.connectionPromise?.finally(() => (this.connectionPromise = undefined));
|
|
16236
|
-
const connectUserResponse = await this.connectionPromise;
|
|
16237
|
-
// connectUserResponse will be void if connectUser called twice for the same user
|
|
16238
|
-
if (connectUserResponse?.me) {
|
|
16239
|
-
this.writeableStateStore.setConnectedUser(connectUserResponse.me);
|
|
16240
|
-
}
|
|
16241
|
-
this.eventHandlersToUnregister.push(this.on('connection.changed', (event) => {
|
|
16242
|
-
if (event.online) {
|
|
16243
|
-
const callsToReWatch = this.writeableStateStore.calls
|
|
16244
|
-
.filter((call) => call.watching)
|
|
16245
|
-
.map((call) => call.cid);
|
|
16246
|
-
this.logger('info', `Rewatching calls after connection changed ${callsToReWatch.join(', ')}`);
|
|
16247
|
-
if (callsToReWatch.length > 0) {
|
|
16248
|
-
this.queryCalls({
|
|
16249
|
-
watch: true,
|
|
16250
|
-
filter_conditions: {
|
|
16251
|
-
cid: { $in: callsToReWatch },
|
|
16252
|
-
},
|
|
16253
|
-
sort: [{ field: 'cid', direction: 1 }],
|
|
16254
|
-
}).catch((err) => {
|
|
16255
|
-
this.logger('error', 'Failed to re-watch calls', err);
|
|
16256
|
-
});
|
|
16257
|
-
}
|
|
16258
|
-
}
|
|
16259
|
-
}));
|
|
16260
|
-
this.eventHandlersToUnregister.push(this.on('call.created', (event) => {
|
|
16261
|
-
const { call, members } = event;
|
|
16262
|
-
if (user.id === call.created_by.id) {
|
|
16263
|
-
this.logger('warn', 'Received `call.created` sent by the current user');
|
|
16264
|
-
return;
|
|
16265
|
-
}
|
|
16266
|
-
this.logger('info', `New call created and registered: ${call.cid}`);
|
|
16267
|
-
const newCall = new Call({
|
|
16268
|
-
streamClient: this.streamClient,
|
|
16269
|
-
type: call.type,
|
|
16270
|
-
id: call.id,
|
|
16271
|
-
members,
|
|
16272
|
-
clientStore: this.writeableStateStore,
|
|
16273
|
-
});
|
|
16274
|
-
newCall.state.updateFromCallResponse(call);
|
|
16275
|
-
this.writeableStateStore.registerCall(newCall);
|
|
16276
|
-
}));
|
|
16277
|
-
this.eventHandlersToUnregister.push(this.on('call.ring', async (event) => {
|
|
16278
|
-
const { call, members } = event;
|
|
16279
|
-
if (user.id === call.created_by.id) {
|
|
16280
|
-
this.logger('debug', 'Received `call.ring` sent by the current user so ignoring the event');
|
|
16281
|
-
return;
|
|
16282
|
-
}
|
|
16283
|
-
// The call might already be tracked by the client,
|
|
16284
|
-
// if `call.created` was received before `call.ring`.
|
|
16285
|
-
// In that case, we cleanup the already tracked call.
|
|
16286
|
-
const prevCall = this.writeableStateStore.findCall(call.type, call.id);
|
|
16287
|
-
await prevCall?.leave({ reason: 'cleaning-up in call.ring' });
|
|
16288
|
-
// we create a new call
|
|
16289
|
-
const theCall = new Call({
|
|
16290
|
-
streamClient: this.streamClient,
|
|
16291
|
-
type: call.type,
|
|
16292
|
-
id: call.id,
|
|
16293
|
-
members,
|
|
16294
|
-
clientStore: this.writeableStateStore,
|
|
16295
|
-
ringing: true,
|
|
16296
|
-
});
|
|
16297
|
-
theCall.state.updateFromCallResponse(call);
|
|
16298
|
-
// we fetch the latest metadata for the call from the server
|
|
16299
|
-
await theCall.get();
|
|
16300
|
-
this.writeableStateStore.registerCall(theCall);
|
|
16301
|
-
}));
|
|
16302
|
-
return connectUserResponse;
|
|
16303
|
-
};
|
|
16304
15712
|
/**
|
|
16305
15713
|
* Disconnects the currently connected user from the client.
|
|
16306
15714
|
*
|
|
@@ -16313,12 +15721,16 @@ class StreamVideoClient {
|
|
|
16313
15721
|
if (!this.streamClient.user && !this.connectionPromise) {
|
|
16314
15722
|
return;
|
|
16315
15723
|
}
|
|
15724
|
+
const userId = this.streamClient.user?.id;
|
|
16316
15725
|
const disconnectUser = () => this.streamClient.disconnectUser(timeout);
|
|
16317
15726
|
this.disconnectionPromise = this.connectionPromise
|
|
16318
15727
|
? this.connectionPromise.then(() => disconnectUser())
|
|
16319
15728
|
: disconnectUser();
|
|
16320
15729
|
this.disconnectionPromise.finally(() => (this.disconnectionPromise = undefined));
|
|
16321
15730
|
await this.disconnectionPromise;
|
|
15731
|
+
if (userId) {
|
|
15732
|
+
StreamVideoClient._instanceMap.delete(userId);
|
|
15733
|
+
}
|
|
16322
15734
|
this.eventHandlersToUnregister.forEach((unregister) => unregister());
|
|
16323
15735
|
this.eventHandlersToUnregister = [];
|
|
16324
15736
|
this.writeableStateStore.setConnectedUser(undefined);
|
|
@@ -16385,7 +15797,7 @@ class StreamVideoClient {
|
|
|
16385
15797
|
clientStore: this.writeableStateStore,
|
|
16386
15798
|
});
|
|
16387
15799
|
call.state.updateFromCallResponse(c.call);
|
|
16388
|
-
await call.applyDeviceConfig(
|
|
15800
|
+
await call.applyDeviceConfig();
|
|
16389
15801
|
if (data.watch) {
|
|
16390
15802
|
this.writeableStateStore.registerCall(call);
|
|
16391
15803
|
}
|
|
@@ -16429,17 +15841,6 @@ class StreamVideoClient {
|
|
|
16429
15841
|
...(push_provider_name != null ? { push_provider_name } : {}),
|
|
16430
15842
|
});
|
|
16431
15843
|
};
|
|
16432
|
-
/**
|
|
16433
|
-
* addDevice - Adds a push device for a user.
|
|
16434
|
-
*
|
|
16435
|
-
* @param {string} id the device id
|
|
16436
|
-
* @param {string} push_provider the push provider name (eg. apn, firebase)
|
|
16437
|
-
* @param {string} push_provider_name user provided push provider name
|
|
16438
|
-
* @param {string} [userID] the user id (defaults to current user)
|
|
16439
|
-
*/
|
|
16440
|
-
this.addVoipDevice = async (id, push_provider, push_provider_name, userID) => {
|
|
16441
|
-
return await this.addDevice(id, push_provider, push_provider_name, userID, true);
|
|
16442
|
-
};
|
|
16443
15844
|
/**
|
|
16444
15845
|
* getDevices - Returns the devices associated with a current user
|
|
16445
15846
|
* @param {string} [userID] User ID. Only works on serverside
|
|
@@ -16467,7 +15868,7 @@ class StreamVideoClient {
|
|
|
16467
15868
|
this.onRingingCall = async (call_cid) => {
|
|
16468
15869
|
// if we find the call and is already ringing, we don't need to create a new call
|
|
16469
15870
|
// as client would have received the call.ring state because the app had WS alive when receiving push notifications
|
|
16470
|
-
let call = this.
|
|
15871
|
+
let call = this.readOnlyStateStore.calls.find((c) => c.cid === call_cid && c.ringing);
|
|
16471
15872
|
if (!call) {
|
|
16472
15873
|
// if not it means that WS is not alive when receiving the push notifications and we need to fetch the call
|
|
16473
15874
|
const [callType, callId] = call_cid.split(':');
|
|
@@ -16508,13 +15909,12 @@ class StreamVideoClient {
|
|
|
16508
15909
|
}
|
|
16509
15910
|
setLogger(logger, logLevel);
|
|
16510
15911
|
this.logger = getLogger(['client']);
|
|
16511
|
-
const coordinatorLogger = getLogger(['coordinator']);
|
|
16512
15912
|
if (typeof apiKeyOrArgs === 'string') {
|
|
16513
15913
|
this.streamClient = new StreamClient(apiKeyOrArgs, {
|
|
16514
15914
|
persistUserOnConnectionFailure: true,
|
|
16515
15915
|
...opts,
|
|
16516
15916
|
logLevel,
|
|
16517
|
-
logger:
|
|
15917
|
+
logger: this.logger,
|
|
16518
15918
|
});
|
|
16519
15919
|
}
|
|
16520
15920
|
else {
|
|
@@ -16522,14 +15922,12 @@ class StreamVideoClient {
|
|
|
16522
15922
|
persistUserOnConnectionFailure: true,
|
|
16523
15923
|
...apiKeyOrArgs.options,
|
|
16524
15924
|
logLevel,
|
|
16525
|
-
logger:
|
|
15925
|
+
logger: this.logger,
|
|
16526
15926
|
});
|
|
16527
15927
|
const sdkInfo = getSdkInfo();
|
|
16528
15928
|
if (sdkInfo) {
|
|
16529
|
-
|
|
16530
|
-
|
|
16531
|
-
const userAgent = this.streamClient.getUserAgent();
|
|
16532
|
-
this.streamClient.setUserAgent(`${userAgent}-video-${sdkName}-sdk-${sdkVersion}`);
|
|
15929
|
+
this.streamClient.setUserAgent(this.streamClient.getUserAgent() +
|
|
15930
|
+
`-video-${SdkType[sdkInfo.type].toLowerCase()}-sdk-${sdkInfo.major}.${sdkInfo.minor}.${sdkInfo.patch}`);
|
|
16533
15931
|
}
|
|
16534
15932
|
}
|
|
16535
15933
|
this.writeableStateStore = new StreamVideoWriteableStateStore();
|
|
@@ -16538,19 +15936,156 @@ class StreamVideoClient {
|
|
|
16538
15936
|
const user = apiKeyOrArgs.user;
|
|
16539
15937
|
const token = apiKeyOrArgs.token || apiKeyOrArgs.tokenProvider;
|
|
16540
15938
|
if (user) {
|
|
15939
|
+
let id = user.id;
|
|
15940
|
+
if (user.type === 'anonymous') {
|
|
15941
|
+
id = '!anon';
|
|
15942
|
+
}
|
|
15943
|
+
if (id) {
|
|
15944
|
+
if (StreamVideoClient._instanceMap.has(apiKeyOrArgs.apiKey + id)) {
|
|
15945
|
+
this.logger('warn', `A StreamVideoClient already exists for ${user.type === 'anonymous' ? 'an anyonymous user' : id}; Prefer using getOrCreateInstance method`);
|
|
15946
|
+
}
|
|
15947
|
+
user.id = id;
|
|
15948
|
+
StreamVideoClient._instanceMap.set(apiKeyOrArgs.apiKey + id, this);
|
|
15949
|
+
}
|
|
16541
15950
|
this.connectUser(user, token).catch((err) => {
|
|
16542
15951
|
this.logger('error', 'Failed to connect', err);
|
|
16543
15952
|
});
|
|
16544
15953
|
}
|
|
16545
15954
|
}
|
|
16546
15955
|
}
|
|
15956
|
+
static getOrCreateInstance(args) {
|
|
15957
|
+
const user = args.user;
|
|
15958
|
+
if (!user.id) {
|
|
15959
|
+
if (args.user.type === 'anonymous') {
|
|
15960
|
+
user.id = '!anon';
|
|
15961
|
+
}
|
|
15962
|
+
else {
|
|
15963
|
+
throw new Error('User ID is required for a non-anonymous user');
|
|
15964
|
+
}
|
|
15965
|
+
}
|
|
15966
|
+
if (!args.token && !args.tokenProvider) {
|
|
15967
|
+
if (args.user.type !== 'anonymous' && args.user.type !== 'guest') {
|
|
15968
|
+
throw new Error('TokenProvider or token is required for a user that is not a guest or anonymous');
|
|
15969
|
+
}
|
|
15970
|
+
}
|
|
15971
|
+
let instance = StreamVideoClient._instanceMap.get(args.apiKey + user.id);
|
|
15972
|
+
if (!instance) {
|
|
15973
|
+
instance = new StreamVideoClient({ ...args, user });
|
|
15974
|
+
}
|
|
15975
|
+
return instance;
|
|
15976
|
+
}
|
|
16547
15977
|
/**
|
|
16548
15978
|
* Return the reactive state store, use this if you want to be notified about changes to the client state
|
|
16549
15979
|
*/
|
|
16550
15980
|
get state() {
|
|
16551
15981
|
return this.readOnlyStateStore;
|
|
16552
15982
|
}
|
|
15983
|
+
/**
|
|
15984
|
+
* Connects the given user to the client.
|
|
15985
|
+
* Only one user can connect at a time, if you want to change users, call `disconnectUser` before connecting a new user.
|
|
15986
|
+
* If the connection is successful, the connected user [state variable](#readonlystatestore) will be updated accordingly.
|
|
15987
|
+
*
|
|
15988
|
+
* @param user the user to connect.
|
|
15989
|
+
* @param token a token or a function that returns a token.
|
|
15990
|
+
*/
|
|
15991
|
+
async connectUser(user, token) {
|
|
15992
|
+
if (user.type === 'anonymous') {
|
|
15993
|
+
user.id = '!anon';
|
|
15994
|
+
return this.connectAnonymousUser(user, token);
|
|
15995
|
+
}
|
|
15996
|
+
let connectUser = () => {
|
|
15997
|
+
return this.streamClient.connectUser(user, token);
|
|
15998
|
+
};
|
|
15999
|
+
if (user.type === 'guest') {
|
|
16000
|
+
connectUser = async () => {
|
|
16001
|
+
return this.streamClient.connectGuestUser(user);
|
|
16002
|
+
};
|
|
16003
|
+
}
|
|
16004
|
+
this.connectionPromise = this.disconnectionPromise
|
|
16005
|
+
? this.disconnectionPromise.then(() => connectUser())
|
|
16006
|
+
: connectUser();
|
|
16007
|
+
this.connectionPromise?.finally(() => (this.connectionPromise = undefined));
|
|
16008
|
+
const connectUserResponse = await this.connectionPromise;
|
|
16009
|
+
// connectUserResponse will be void if connectUser called twice for the same user
|
|
16010
|
+
if (connectUserResponse?.me) {
|
|
16011
|
+
this.writeableStateStore.setConnectedUser(connectUserResponse.me);
|
|
16012
|
+
}
|
|
16013
|
+
this.eventHandlersToUnregister.push(this.on('connection.changed', (event) => {
|
|
16014
|
+
if (event.online) {
|
|
16015
|
+
const callsToReWatch = this.writeableStateStore.calls
|
|
16016
|
+
.filter((call) => call.watching)
|
|
16017
|
+
.map((call) => call.cid);
|
|
16018
|
+
this.logger('info', `Rewatching calls after connection changed ${callsToReWatch.join(', ')}`);
|
|
16019
|
+
if (callsToReWatch.length > 0) {
|
|
16020
|
+
this.queryCalls({
|
|
16021
|
+
watch: true,
|
|
16022
|
+
filter_conditions: {
|
|
16023
|
+
cid: { $in: callsToReWatch },
|
|
16024
|
+
},
|
|
16025
|
+
sort: [{ field: 'cid', direction: 1 }],
|
|
16026
|
+
}).catch((err) => {
|
|
16027
|
+
this.logger('error', 'Failed to re-watch calls', err);
|
|
16028
|
+
});
|
|
16029
|
+
}
|
|
16030
|
+
}
|
|
16031
|
+
}));
|
|
16032
|
+
this.eventHandlersToUnregister.push(this.on('call.created', (event) => {
|
|
16033
|
+
const { call, members } = event;
|
|
16034
|
+
if (user.id === call.created_by.id) {
|
|
16035
|
+
this.logger('warn', 'Received `call.created` sent by the current user');
|
|
16036
|
+
return;
|
|
16037
|
+
}
|
|
16038
|
+
this.logger('info', `New call created and registered: ${call.cid}`);
|
|
16039
|
+
const newCall = new Call({
|
|
16040
|
+
streamClient: this.streamClient,
|
|
16041
|
+
type: call.type,
|
|
16042
|
+
id: call.id,
|
|
16043
|
+
members,
|
|
16044
|
+
clientStore: this.writeableStateStore,
|
|
16045
|
+
});
|
|
16046
|
+
newCall.state.updateFromCallResponse(call);
|
|
16047
|
+
this.writeableStateStore.registerCall(newCall);
|
|
16048
|
+
}));
|
|
16049
|
+
this.eventHandlersToUnregister.push(this.on('call.ring', async (event) => {
|
|
16050
|
+
const { call, members } = event;
|
|
16051
|
+
if (user.id === call.created_by.id) {
|
|
16052
|
+
this.logger('debug', 'Received `call.ring` sent by the current user so ignoring the event');
|
|
16053
|
+
return;
|
|
16054
|
+
}
|
|
16055
|
+
// The call might already be tracked by the client,
|
|
16056
|
+
// if `call.created` was received before `call.ring`.
|
|
16057
|
+
// In that case, we cleanup the already tracked call.
|
|
16058
|
+
const prevCall = this.writeableStateStore.findCall(call.type, call.id);
|
|
16059
|
+
await prevCall?.leave({ reason: 'cleaning-up in call.ring' });
|
|
16060
|
+
// we create a new call
|
|
16061
|
+
const theCall = new Call({
|
|
16062
|
+
streamClient: this.streamClient,
|
|
16063
|
+
type: call.type,
|
|
16064
|
+
id: call.id,
|
|
16065
|
+
members,
|
|
16066
|
+
clientStore: this.writeableStateStore,
|
|
16067
|
+
ringing: true,
|
|
16068
|
+
});
|
|
16069
|
+
theCall.state.updateFromCallResponse(call);
|
|
16070
|
+
// we fetch the latest metadata for the call from the server
|
|
16071
|
+
await theCall.get();
|
|
16072
|
+
this.writeableStateStore.registerCall(theCall);
|
|
16073
|
+
}));
|
|
16074
|
+
return connectUserResponse;
|
|
16075
|
+
}
|
|
16076
|
+
/**
|
|
16077
|
+
* addDevice - Adds a push device for a user.
|
|
16078
|
+
*
|
|
16079
|
+
* @param {string} id the device id
|
|
16080
|
+
* @param {string} push_provider the push provider name (eg. apn, firebase)
|
|
16081
|
+
* @param {string} push_provider_name user provided push provider name
|
|
16082
|
+
* @param {string} [userID] the user id (defaults to current user)
|
|
16083
|
+
*/
|
|
16084
|
+
async addVoipDevice(id, push_provider, push_provider_name, userID) {
|
|
16085
|
+
return await this.addDevice(id, push_provider, push_provider_name, userID, true);
|
|
16086
|
+
}
|
|
16553
16087
|
}
|
|
16088
|
+
StreamVideoClient._instanceMap = new Map();
|
|
16554
16089
|
|
|
16555
|
-
export { AudioSettingsRequestDefaultDeviceEnum, AudioSettingsResponseDefaultDeviceEnum, BlockListOptionsBehaviorEnum, browsers as Browsers, Call, CallState, CallType, CallTypes, CallingState, CameraManager, CameraManagerState, ChannelConfigWithInfoAutomodBehaviorEnum, ChannelConfigWithInfoAutomodEnum, ChannelConfigWithInfoBlocklistBehaviorEnum, CreateDeviceRequestPushProviderEnum, DebounceType, DynascaleManager, ErrorFromResponse, InputMediaDeviceManager, InputMediaDeviceManagerState, MicrophoneManager, MicrophoneManagerState, NoiseCancellationSettingsModeEnum, OwnCapability, RecordSettingsRequestModeEnum, RecordSettingsRequestQualityEnum, rxUtils as RxUtils, ScreenShareManager, ScreenShareState, events as SfuEvents, models as SfuModels, SpeakerManager, SpeakerState, StreamSfuClient, StreamVideoClient, StreamVideoReadOnlyStateStore, StreamVideoWriteableStateStore, TranscriptionSettingsRequestModeEnum, TranscriptionSettingsResponseModeEnum, VideoSettingsRequestCameraFacingEnum, VideoSettingsResponseCameraFacingEnum, ViewportTracker, VisibilityState, checkIfAudioOutputChangeSupported, combineComparators, conditional, createSoundDetector, defaultSortPreset, descending, deviceIds$, disposeOfMediaStream, dominantSpeaker, getAudioBrowserPermission, getAudioDevices, getAudioOutputDevices, getAudioStream, getClientDetails, getDeviceInfo,
|
|
16090
|
+
export { AudioSettingsRequestDefaultDeviceEnum, AudioSettingsResponseDefaultDeviceEnum, BlockListOptionsBehaviorEnum, browsers as Browsers, Call, CallState, CallType, CallTypes, CallingState, CameraManager, CameraManagerState, ChannelConfigWithInfoAutomodBehaviorEnum, ChannelConfigWithInfoAutomodEnum, ChannelConfigWithInfoBlocklistBehaviorEnum, CreateDeviceRequestPushProviderEnum, DebounceType, DynascaleManager, ErrorFromResponse, InputMediaDeviceManager, InputMediaDeviceManagerState, MicrophoneManager, MicrophoneManagerState, NoiseCancellationSettingsModeEnum, OwnCapability, RecordSettingsRequestModeEnum, RecordSettingsRequestQualityEnum, rxUtils as RxUtils, ScreenShareManager, ScreenShareState, events as SfuEvents, models as SfuModels, SpeakerManager, SpeakerState, StreamSfuClient, StreamVideoClient, StreamVideoReadOnlyStateStore, StreamVideoWriteableStateStore, TranscriptionSettingsRequestModeEnum, TranscriptionSettingsResponseModeEnum, VideoSettingsRequestCameraFacingEnum, VideoSettingsResponseCameraFacingEnum, ViewportTracker, VisibilityState, checkIfAudioOutputChangeSupported, combineComparators, conditional, createSoundDetector, defaultSortPreset, descending, deviceIds$, disposeOfMediaStream, dominantSpeaker, getAudioBrowserPermission, getAudioDevices, getAudioOutputDevices, getAudioStream, getClientDetails, getDeviceInfo, getLogger, getOSInfo, getScreenShareStream, getSdkInfo, getVideoBrowserPermission, getVideoDevices, getVideoStream, getWebRTCInfo, hasAudio, hasScreenShare, hasScreenShareAudio, hasVideo, isPinned, livestreamOrAudioRoomSortPreset, logLevels, logToConsole, name, noopComparator, paginatedLayoutSortPreset, pinned, publishingAudio, publishingVideo, reactionType, role, screenSharing, setDeviceInfo, setLogLevel, setLogger, setOSInfo, setSdkInfo, setWebRTCInfo, speakerLayoutSortPreset, speaking };
|
|
16556
16091
|
//# sourceMappingURL=index.es.js.map
|