@stream-io/video-client 1.5.0 → 1.6.0-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 +175 -0
- package/dist/index.browser.es.js +1986 -1482
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +1983 -1478
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +1986 -1482
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +93 -9
- package/dist/src/StreamSfuClient.d.ts +73 -56
- package/dist/src/StreamVideoClient.d.ts +2 -2
- package/dist/src/coordinator/connection/client.d.ts +3 -4
- package/dist/src/coordinator/connection/types.d.ts +5 -1
- package/dist/src/devices/InputMediaDeviceManager.d.ts +4 -0
- package/dist/src/devices/MicrophoneManager.d.ts +1 -1
- package/dist/src/events/callEventHandlers.d.ts +1 -3
- package/dist/src/events/internal.d.ts +4 -0
- package/dist/src/gen/video/sfu/event/events.d.ts +106 -4
- package/dist/src/gen/video/sfu/models/models.d.ts +64 -65
- package/dist/src/helpers/ensureExhausted.d.ts +1 -0
- package/dist/src/helpers/withResolvers.d.ts +14 -0
- package/dist/src/logger.d.ts +1 -0
- package/dist/src/rpc/createClient.d.ts +2 -0
- package/dist/src/rpc/index.d.ts +1 -0
- package/dist/src/rpc/retryable.d.ts +23 -0
- package/dist/src/rtc/Dispatcher.d.ts +1 -1
- package/dist/src/rtc/IceTrickleBuffer.d.ts +0 -1
- package/dist/src/rtc/Publisher.d.ts +24 -25
- package/dist/src/rtc/Subscriber.d.ts +12 -11
- package/dist/src/rtc/helpers/rtcConfiguration.d.ts +2 -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 +46 -2
- package/package.json +3 -3
- package/src/Call.ts +628 -566
- package/src/StreamSfuClient.ts +276 -246
- package/src/StreamVideoClient.ts +15 -16
- package/src/coordinator/connection/client.ts +25 -8
- package/src/coordinator/connection/connection.ts +1 -0
- package/src/coordinator/connection/types.ts +6 -0
- package/src/devices/CameraManager.ts +1 -1
- package/src/devices/InputMediaDeviceManager.ts +12 -3
- package/src/devices/MicrophoneManager.ts +3 -3
- package/src/devices/devices.ts +1 -1
- package/src/events/__tests__/mutes.test.ts +10 -13
- package/src/events/__tests__/participant.test.ts +75 -0
- package/src/events/callEventHandlers.ts +4 -7
- package/src/events/internal.ts +20 -3
- package/src/events/mutes.ts +5 -3
- package/src/events/participant.ts +48 -15
- package/src/gen/video/sfu/event/events.ts +451 -8
- package/src/gen/video/sfu/models/models.ts +211 -204
- package/src/helpers/ensureExhausted.ts +5 -0
- package/src/helpers/withResolvers.ts +43 -0
- package/src/logger.ts +3 -1
- package/src/rpc/__tests__/retryable.test.ts +72 -0
- package/src/rpc/createClient.ts +21 -0
- package/src/rpc/index.ts +1 -0
- package/src/rpc/retryable.ts +57 -0
- package/src/rtc/Dispatcher.ts +6 -2
- package/src/rtc/IceTrickleBuffer.ts +2 -2
- package/src/rtc/Publisher.ts +127 -163
- package/src/rtc/Subscriber.ts +92 -155
- package/src/rtc/__tests__/Publisher.test.ts +18 -95
- package/src/rtc/__tests__/Subscriber.test.ts +63 -99
- package/src/rtc/__tests__/videoLayers.test.ts +2 -2
- package/src/rtc/helpers/rtcConfiguration.ts +11 -0
- package/src/rtc/helpers/tracks.ts +27 -7
- package/src/rtc/signal.ts +3 -3
- package/src/rtc/videoLayers.ts +1 -10
- package/src/stats/SfuStatsReporter.ts +1 -0
- package/src/store/CallState.ts +109 -2
- package/src/store/__tests__/CallState.test.ts +48 -37
- package/dist/src/rtc/flows/join.d.ts +0 -20
- package/src/rtc/flows/join.ts +0 -65
package/dist/index.cjs.js
CHANGED
|
@@ -8,8 +8,8 @@ var twirpTransport = require('@protobuf-ts/twirp-transport');
|
|
|
8
8
|
var rxjs = require('rxjs');
|
|
9
9
|
var SDP = require('sdp-transform');
|
|
10
10
|
var uaParserJs = require('ua-parser-js');
|
|
11
|
-
var WebSocket = require('isomorphic-ws');
|
|
12
11
|
var https = require('https');
|
|
12
|
+
var WebSocket$1 = require('isomorphic-ws');
|
|
13
13
|
var base64Js = require('base64-js');
|
|
14
14
|
|
|
15
15
|
function _interopNamespaceDefault(e) {
|
|
@@ -1035,6 +1035,10 @@ var CallEndedReason;
|
|
|
1035
1035
|
* @generated from protobuf enum value: CALL_ENDED_REASON_KICKED = 3;
|
|
1036
1036
|
*/
|
|
1037
1037
|
CallEndedReason[CallEndedReason["KICKED"] = 3] = "KICKED";
|
|
1038
|
+
/**
|
|
1039
|
+
* @generated from protobuf enum value: CALL_ENDED_REASON_SESSION_ENDED = 4;
|
|
1040
|
+
*/
|
|
1041
|
+
CallEndedReason[CallEndedReason["SESSION_ENDED"] = 4] = "SESSION_ENDED";
|
|
1038
1042
|
})(CallEndedReason || (CallEndedReason = {}));
|
|
1039
1043
|
/**
|
|
1040
1044
|
* WebsocketReconnectStrategy defines the ws strategies available for handling reconnections.
|
|
@@ -1048,7 +1052,7 @@ var WebsocketReconnectStrategy;
|
|
|
1048
1052
|
*/
|
|
1049
1053
|
WebsocketReconnectStrategy[WebsocketReconnectStrategy["UNSPECIFIED"] = 0] = "UNSPECIFIED";
|
|
1050
1054
|
/**
|
|
1051
|
-
* Sent after reaching the maximum reconnection attempts, leading to permanent disconnect.
|
|
1055
|
+
* Sent after reaching the maximum reconnection attempts, or any other unrecoverable error leading to permanent disconnect.
|
|
1052
1056
|
*
|
|
1053
1057
|
* @generated from protobuf enum value: WEBSOCKET_RECONNECT_STRATEGY_DISCONNECT = 1;
|
|
1054
1058
|
*/
|
|
@@ -1061,25 +1065,18 @@ var WebsocketReconnectStrategy;
|
|
|
1061
1065
|
*/
|
|
1062
1066
|
WebsocketReconnectStrategy[WebsocketReconnectStrategy["FAST"] = 2] = "FAST";
|
|
1063
1067
|
/**
|
|
1064
|
-
* SDK should
|
|
1065
|
-
* ensuring a clean state for the reconnection.
|
|
1066
|
-
*
|
|
1067
|
-
* @generated from protobuf enum value: WEBSOCKET_RECONNECT_STRATEGY_CLEAN = 3;
|
|
1068
|
-
*/
|
|
1069
|
-
WebsocketReconnectStrategy[WebsocketReconnectStrategy["CLEAN"] = 3] = "CLEAN";
|
|
1070
|
-
/**
|
|
1071
|
-
* SDK should obtain new credentials from the coordinator, drops existing pc instances, and initializes
|
|
1068
|
+
* SDK should obtain new credentials from the coordinator, drops existing pc instances, set a new session_id and initializes
|
|
1072
1069
|
* a completely new WebSocket connection, ensuring a comprehensive reset.
|
|
1073
1070
|
*
|
|
1074
|
-
* @generated from protobuf enum value:
|
|
1071
|
+
* @generated from protobuf enum value: WEBSOCKET_RECONNECT_STRATEGY_REJOIN = 3;
|
|
1075
1072
|
*/
|
|
1076
|
-
WebsocketReconnectStrategy[WebsocketReconnectStrategy["
|
|
1073
|
+
WebsocketReconnectStrategy[WebsocketReconnectStrategy["REJOIN"] = 3] = "REJOIN";
|
|
1077
1074
|
/**
|
|
1078
1075
|
* SDK should migrate to a new SFU instance
|
|
1079
1076
|
*
|
|
1080
|
-
* @generated from protobuf enum value: WEBSOCKET_RECONNECT_STRATEGY_MIGRATE =
|
|
1077
|
+
* @generated from protobuf enum value: WEBSOCKET_RECONNECT_STRATEGY_MIGRATE = 4;
|
|
1081
1078
|
*/
|
|
1082
|
-
WebsocketReconnectStrategy[WebsocketReconnectStrategy["MIGRATE"] =
|
|
1079
|
+
WebsocketReconnectStrategy[WebsocketReconnectStrategy["MIGRATE"] = 4] = "MIGRATE";
|
|
1083
1080
|
})(WebsocketReconnectStrategy || (WebsocketReconnectStrategy = {}));
|
|
1084
1081
|
// @generated message type with reflection information, may provide speed optimized methods
|
|
1085
1082
|
class CallState$Type extends runtime.MessageType {
|
|
@@ -1881,6 +1878,7 @@ class TrackInfo$Type extends runtime.MessageType {
|
|
|
1881
1878
|
{ no: 7, name: 'dtx', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
|
|
1882
1879
|
{ no: 8, name: 'stereo', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
|
|
1883
1880
|
{ no: 9, name: 'red', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
|
|
1881
|
+
{ no: 10, name: 'muted', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
|
|
1884
1882
|
]);
|
|
1885
1883
|
}
|
|
1886
1884
|
create(value) {
|
|
@@ -1892,6 +1890,7 @@ class TrackInfo$Type extends runtime.MessageType {
|
|
|
1892
1890
|
message.dtx = false;
|
|
1893
1891
|
message.stereo = false;
|
|
1894
1892
|
message.red = false;
|
|
1893
|
+
message.muted = false;
|
|
1895
1894
|
if (value !== undefined)
|
|
1896
1895
|
runtime.reflectionMergePartial(this, message, value);
|
|
1897
1896
|
return message;
|
|
@@ -1922,6 +1921,9 @@ class TrackInfo$Type extends runtime.MessageType {
|
|
|
1922
1921
|
case /* bool red */ 9:
|
|
1923
1922
|
message.red = reader.bool();
|
|
1924
1923
|
break;
|
|
1924
|
+
case /* bool muted */ 10:
|
|
1925
|
+
message.muted = reader.bool();
|
|
1926
|
+
break;
|
|
1925
1927
|
default:
|
|
1926
1928
|
let u = options.readUnknownField;
|
|
1927
1929
|
if (u === 'throw')
|
|
@@ -1955,6 +1957,9 @@ class TrackInfo$Type extends runtime.MessageType {
|
|
|
1955
1957
|
/* bool red = 9; */
|
|
1956
1958
|
if (message.red !== false)
|
|
1957
1959
|
writer.tag(9, runtime.WireType.Varint).bool(message.red);
|
|
1960
|
+
/* bool muted = 10; */
|
|
1961
|
+
if (message.muted !== false)
|
|
1962
|
+
writer.tag(10, runtime.WireType.Varint).bool(message.muted);
|
|
1958
1963
|
let u = options.writeUnknownFields;
|
|
1959
1964
|
if (u !== false)
|
|
1960
1965
|
(u == true ? runtime.UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
|
@@ -1966,108 +1971,6 @@ class TrackInfo$Type extends runtime.MessageType {
|
|
|
1966
1971
|
*/
|
|
1967
1972
|
const TrackInfo = new TrackInfo$Type();
|
|
1968
1973
|
// @generated message type with reflection information, may provide speed optimized methods
|
|
1969
|
-
class Call$Type extends runtime.MessageType {
|
|
1970
|
-
constructor() {
|
|
1971
|
-
super('stream.video.sfu.models.Call', [
|
|
1972
|
-
{ no: 1, name: 'type', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
|
|
1973
|
-
{ no: 2, name: 'id', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
|
|
1974
|
-
{
|
|
1975
|
-
no: 3,
|
|
1976
|
-
name: 'created_by_user_id',
|
|
1977
|
-
kind: 'scalar',
|
|
1978
|
-
T: 9 /*ScalarType.STRING*/,
|
|
1979
|
-
},
|
|
1980
|
-
{
|
|
1981
|
-
no: 4,
|
|
1982
|
-
name: 'host_user_id',
|
|
1983
|
-
kind: 'scalar',
|
|
1984
|
-
T: 9 /*ScalarType.STRING*/,
|
|
1985
|
-
},
|
|
1986
|
-
{ no: 5, name: 'custom', kind: 'message', T: () => Struct },
|
|
1987
|
-
{ no: 6, name: 'created_at', kind: 'message', T: () => Timestamp },
|
|
1988
|
-
{ no: 7, name: 'updated_at', kind: 'message', T: () => Timestamp },
|
|
1989
|
-
]);
|
|
1990
|
-
}
|
|
1991
|
-
create(value) {
|
|
1992
|
-
const message = globalThis.Object.create(this.messagePrototype);
|
|
1993
|
-
message.type = '';
|
|
1994
|
-
message.id = '';
|
|
1995
|
-
message.createdByUserId = '';
|
|
1996
|
-
message.hostUserId = '';
|
|
1997
|
-
if (value !== undefined)
|
|
1998
|
-
runtime.reflectionMergePartial(this, message, value);
|
|
1999
|
-
return message;
|
|
2000
|
-
}
|
|
2001
|
-
internalBinaryRead(reader, length, options, target) {
|
|
2002
|
-
let message = target ?? this.create(), end = reader.pos + length;
|
|
2003
|
-
while (reader.pos < end) {
|
|
2004
|
-
let [fieldNo, wireType] = reader.tag();
|
|
2005
|
-
switch (fieldNo) {
|
|
2006
|
-
case /* string type */ 1:
|
|
2007
|
-
message.type = reader.string();
|
|
2008
|
-
break;
|
|
2009
|
-
case /* string id */ 2:
|
|
2010
|
-
message.id = reader.string();
|
|
2011
|
-
break;
|
|
2012
|
-
case /* string created_by_user_id */ 3:
|
|
2013
|
-
message.createdByUserId = reader.string();
|
|
2014
|
-
break;
|
|
2015
|
-
case /* string host_user_id */ 4:
|
|
2016
|
-
message.hostUserId = reader.string();
|
|
2017
|
-
break;
|
|
2018
|
-
case /* google.protobuf.Struct custom */ 5:
|
|
2019
|
-
message.custom = Struct.internalBinaryRead(reader, reader.uint32(), options, message.custom);
|
|
2020
|
-
break;
|
|
2021
|
-
case /* google.protobuf.Timestamp created_at */ 6:
|
|
2022
|
-
message.createdAt = Timestamp.internalBinaryRead(reader, reader.uint32(), options, message.createdAt);
|
|
2023
|
-
break;
|
|
2024
|
-
case /* google.protobuf.Timestamp updated_at */ 7:
|
|
2025
|
-
message.updatedAt = Timestamp.internalBinaryRead(reader, reader.uint32(), options, message.updatedAt);
|
|
2026
|
-
break;
|
|
2027
|
-
default:
|
|
2028
|
-
let u = options.readUnknownField;
|
|
2029
|
-
if (u === 'throw')
|
|
2030
|
-
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
|
2031
|
-
let d = reader.skip(wireType);
|
|
2032
|
-
if (u !== false)
|
|
2033
|
-
(u === true ? runtime.UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
|
2034
|
-
}
|
|
2035
|
-
}
|
|
2036
|
-
return message;
|
|
2037
|
-
}
|
|
2038
|
-
internalBinaryWrite(message, writer, options) {
|
|
2039
|
-
/* string type = 1; */
|
|
2040
|
-
if (message.type !== '')
|
|
2041
|
-
writer.tag(1, runtime.WireType.LengthDelimited).string(message.type);
|
|
2042
|
-
/* string id = 2; */
|
|
2043
|
-
if (message.id !== '')
|
|
2044
|
-
writer.tag(2, runtime.WireType.LengthDelimited).string(message.id);
|
|
2045
|
-
/* string created_by_user_id = 3; */
|
|
2046
|
-
if (message.createdByUserId !== '')
|
|
2047
|
-
writer.tag(3, runtime.WireType.LengthDelimited).string(message.createdByUserId);
|
|
2048
|
-
/* string host_user_id = 4; */
|
|
2049
|
-
if (message.hostUserId !== '')
|
|
2050
|
-
writer.tag(4, runtime.WireType.LengthDelimited).string(message.hostUserId);
|
|
2051
|
-
/* google.protobuf.Struct custom = 5; */
|
|
2052
|
-
if (message.custom)
|
|
2053
|
-
Struct.internalBinaryWrite(message.custom, writer.tag(5, runtime.WireType.LengthDelimited).fork(), options).join();
|
|
2054
|
-
/* google.protobuf.Timestamp created_at = 6; */
|
|
2055
|
-
if (message.createdAt)
|
|
2056
|
-
Timestamp.internalBinaryWrite(message.createdAt, writer.tag(6, runtime.WireType.LengthDelimited).fork(), options).join();
|
|
2057
|
-
/* google.protobuf.Timestamp updated_at = 7; */
|
|
2058
|
-
if (message.updatedAt)
|
|
2059
|
-
Timestamp.internalBinaryWrite(message.updatedAt, writer.tag(7, runtime.WireType.LengthDelimited).fork(), options).join();
|
|
2060
|
-
let u = options.writeUnknownFields;
|
|
2061
|
-
if (u !== false)
|
|
2062
|
-
(u == true ? runtime.UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
|
2063
|
-
return writer;
|
|
2064
|
-
}
|
|
2065
|
-
}
|
|
2066
|
-
/**
|
|
2067
|
-
* @generated MessageType for protobuf message stream.video.sfu.models.Call
|
|
2068
|
-
*/
|
|
2069
|
-
const Call$1 = new Call$Type();
|
|
2070
|
-
// @generated message type with reflection information, may provide speed optimized methods
|
|
2071
1974
|
let Error$Type$1 = class Error$Type extends runtime.MessageType {
|
|
2072
1975
|
constructor() {
|
|
2073
1976
|
super('stream.video.sfu.models.Error', [
|
|
@@ -2461,6 +2364,108 @@ class Device$Type extends runtime.MessageType {
|
|
|
2461
2364
|
*/
|
|
2462
2365
|
const Device = new Device$Type();
|
|
2463
2366
|
// @generated message type with reflection information, may provide speed optimized methods
|
|
2367
|
+
class Call$Type extends runtime.MessageType {
|
|
2368
|
+
constructor() {
|
|
2369
|
+
super('stream.video.sfu.models.Call', [
|
|
2370
|
+
{ no: 1, name: 'type', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
|
|
2371
|
+
{ no: 2, name: 'id', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
|
|
2372
|
+
{
|
|
2373
|
+
no: 3,
|
|
2374
|
+
name: 'created_by_user_id',
|
|
2375
|
+
kind: 'scalar',
|
|
2376
|
+
T: 9 /*ScalarType.STRING*/,
|
|
2377
|
+
},
|
|
2378
|
+
{
|
|
2379
|
+
no: 4,
|
|
2380
|
+
name: 'host_user_id',
|
|
2381
|
+
kind: 'scalar',
|
|
2382
|
+
T: 9 /*ScalarType.STRING*/,
|
|
2383
|
+
},
|
|
2384
|
+
{ no: 5, name: 'custom', kind: 'message', T: () => Struct },
|
|
2385
|
+
{ no: 6, name: 'created_at', kind: 'message', T: () => Timestamp },
|
|
2386
|
+
{ no: 7, name: 'updated_at', kind: 'message', T: () => Timestamp },
|
|
2387
|
+
]);
|
|
2388
|
+
}
|
|
2389
|
+
create(value) {
|
|
2390
|
+
const message = globalThis.Object.create(this.messagePrototype);
|
|
2391
|
+
message.type = '';
|
|
2392
|
+
message.id = '';
|
|
2393
|
+
message.createdByUserId = '';
|
|
2394
|
+
message.hostUserId = '';
|
|
2395
|
+
if (value !== undefined)
|
|
2396
|
+
runtime.reflectionMergePartial(this, message, value);
|
|
2397
|
+
return message;
|
|
2398
|
+
}
|
|
2399
|
+
internalBinaryRead(reader, length, options, target) {
|
|
2400
|
+
let message = target ?? this.create(), end = reader.pos + length;
|
|
2401
|
+
while (reader.pos < end) {
|
|
2402
|
+
let [fieldNo, wireType] = reader.tag();
|
|
2403
|
+
switch (fieldNo) {
|
|
2404
|
+
case /* string type */ 1:
|
|
2405
|
+
message.type = reader.string();
|
|
2406
|
+
break;
|
|
2407
|
+
case /* string id */ 2:
|
|
2408
|
+
message.id = reader.string();
|
|
2409
|
+
break;
|
|
2410
|
+
case /* string created_by_user_id */ 3:
|
|
2411
|
+
message.createdByUserId = reader.string();
|
|
2412
|
+
break;
|
|
2413
|
+
case /* string host_user_id */ 4:
|
|
2414
|
+
message.hostUserId = reader.string();
|
|
2415
|
+
break;
|
|
2416
|
+
case /* google.protobuf.Struct custom */ 5:
|
|
2417
|
+
message.custom = Struct.internalBinaryRead(reader, reader.uint32(), options, message.custom);
|
|
2418
|
+
break;
|
|
2419
|
+
case /* google.protobuf.Timestamp created_at */ 6:
|
|
2420
|
+
message.createdAt = Timestamp.internalBinaryRead(reader, reader.uint32(), options, message.createdAt);
|
|
2421
|
+
break;
|
|
2422
|
+
case /* google.protobuf.Timestamp updated_at */ 7:
|
|
2423
|
+
message.updatedAt = Timestamp.internalBinaryRead(reader, reader.uint32(), options, message.updatedAt);
|
|
2424
|
+
break;
|
|
2425
|
+
default:
|
|
2426
|
+
let u = options.readUnknownField;
|
|
2427
|
+
if (u === 'throw')
|
|
2428
|
+
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
|
2429
|
+
let d = reader.skip(wireType);
|
|
2430
|
+
if (u !== false)
|
|
2431
|
+
(u === true ? runtime.UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
return message;
|
|
2435
|
+
}
|
|
2436
|
+
internalBinaryWrite(message, writer, options) {
|
|
2437
|
+
/* string type = 1; */
|
|
2438
|
+
if (message.type !== '')
|
|
2439
|
+
writer.tag(1, runtime.WireType.LengthDelimited).string(message.type);
|
|
2440
|
+
/* string id = 2; */
|
|
2441
|
+
if (message.id !== '')
|
|
2442
|
+
writer.tag(2, runtime.WireType.LengthDelimited).string(message.id);
|
|
2443
|
+
/* string created_by_user_id = 3; */
|
|
2444
|
+
if (message.createdByUserId !== '')
|
|
2445
|
+
writer.tag(3, runtime.WireType.LengthDelimited).string(message.createdByUserId);
|
|
2446
|
+
/* string host_user_id = 4; */
|
|
2447
|
+
if (message.hostUserId !== '')
|
|
2448
|
+
writer.tag(4, runtime.WireType.LengthDelimited).string(message.hostUserId);
|
|
2449
|
+
/* google.protobuf.Struct custom = 5; */
|
|
2450
|
+
if (message.custom)
|
|
2451
|
+
Struct.internalBinaryWrite(message.custom, writer.tag(5, runtime.WireType.LengthDelimited).fork(), options).join();
|
|
2452
|
+
/* google.protobuf.Timestamp created_at = 6; */
|
|
2453
|
+
if (message.createdAt)
|
|
2454
|
+
Timestamp.internalBinaryWrite(message.createdAt, writer.tag(6, runtime.WireType.LengthDelimited).fork(), options).join();
|
|
2455
|
+
/* google.protobuf.Timestamp updated_at = 7; */
|
|
2456
|
+
if (message.updatedAt)
|
|
2457
|
+
Timestamp.internalBinaryWrite(message.updatedAt, writer.tag(7, runtime.WireType.LengthDelimited).fork(), options).join();
|
|
2458
|
+
let u = options.writeUnknownFields;
|
|
2459
|
+
if (u !== false)
|
|
2460
|
+
(u == true ? runtime.UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
|
2461
|
+
return writer;
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
/**
|
|
2465
|
+
* @generated MessageType for protobuf message stream.video.sfu.models.Call
|
|
2466
|
+
*/
|
|
2467
|
+
const Call$1 = new Call$Type();
|
|
2468
|
+
// @generated message type with reflection information, may provide speed optimized methods
|
|
2464
2469
|
class CallGrants$Type extends runtime.MessageType {
|
|
2465
2470
|
constructor() {
|
|
2466
2471
|
super('stream.video.sfu.models.CallGrants', [
|
|
@@ -3991,6 +3996,13 @@ class SfuEvent$Type extends runtime.MessageType {
|
|
|
3991
3996
|
oneof: 'eventPayload',
|
|
3992
3997
|
T: () => ParticipantUpdated,
|
|
3993
3998
|
},
|
|
3999
|
+
{
|
|
4000
|
+
no: 25,
|
|
4001
|
+
name: 'participant_migration_complete',
|
|
4002
|
+
kind: 'message',
|
|
4003
|
+
oneof: 'eventPayload',
|
|
4004
|
+
T: () => ParticipantMigrationComplete,
|
|
4005
|
+
},
|
|
3994
4006
|
]);
|
|
3995
4007
|
}
|
|
3996
4008
|
create(value) {
|
|
@@ -4125,6 +4137,12 @@ class SfuEvent$Type extends runtime.MessageType {
|
|
|
4125
4137
|
participantUpdated: ParticipantUpdated.internalBinaryRead(reader, reader.uint32(), options, message.eventPayload.participantUpdated),
|
|
4126
4138
|
};
|
|
4127
4139
|
break;
|
|
4140
|
+
case /* stream.video.sfu.event.ParticipantMigrationComplete participant_migration_complete */ 25:
|
|
4141
|
+
message.eventPayload = {
|
|
4142
|
+
oneofKind: 'participantMigrationComplete',
|
|
4143
|
+
participantMigrationComplete: ParticipantMigrationComplete.internalBinaryRead(reader, reader.uint32(), options, message.eventPayload.participantMigrationComplete),
|
|
4144
|
+
};
|
|
4145
|
+
break;
|
|
4128
4146
|
default:
|
|
4129
4147
|
let u = options.readUnknownField;
|
|
4130
4148
|
if (u === 'throw')
|
|
@@ -4197,6 +4215,9 @@ class SfuEvent$Type extends runtime.MessageType {
|
|
|
4197
4215
|
/* stream.video.sfu.event.ParticipantUpdated participant_updated = 24; */
|
|
4198
4216
|
if (message.eventPayload.oneofKind === 'participantUpdated')
|
|
4199
4217
|
ParticipantUpdated.internalBinaryWrite(message.eventPayload.participantUpdated, writer.tag(24, runtime.WireType.LengthDelimited).fork(), options).join();
|
|
4218
|
+
/* stream.video.sfu.event.ParticipantMigrationComplete participant_migration_complete = 25; */
|
|
4219
|
+
if (message.eventPayload.oneofKind === 'participantMigrationComplete')
|
|
4220
|
+
ParticipantMigrationComplete.internalBinaryWrite(message.eventPayload.participantMigrationComplete, writer.tag(25, runtime.WireType.LengthDelimited).fork(), options).join();
|
|
4200
4221
|
let u = options.writeUnknownFields;
|
|
4201
4222
|
if (u !== false)
|
|
4202
4223
|
(u == true ? runtime.UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
|
@@ -4208,6 +4229,31 @@ class SfuEvent$Type extends runtime.MessageType {
|
|
|
4208
4229
|
*/
|
|
4209
4230
|
const SfuEvent = new SfuEvent$Type();
|
|
4210
4231
|
// @generated message type with reflection information, may provide speed optimized methods
|
|
4232
|
+
class ParticipantMigrationComplete$Type extends runtime.MessageType {
|
|
4233
|
+
constructor() {
|
|
4234
|
+
super('stream.video.sfu.event.ParticipantMigrationComplete', []);
|
|
4235
|
+
}
|
|
4236
|
+
create(value) {
|
|
4237
|
+
const message = globalThis.Object.create(this.messagePrototype);
|
|
4238
|
+
if (value !== undefined)
|
|
4239
|
+
runtime.reflectionMergePartial(this, message, value);
|
|
4240
|
+
return message;
|
|
4241
|
+
}
|
|
4242
|
+
internalBinaryRead(reader, length, options, target) {
|
|
4243
|
+
return target ?? this.create();
|
|
4244
|
+
}
|
|
4245
|
+
internalBinaryWrite(message, writer, options) {
|
|
4246
|
+
let u = options.writeUnknownFields;
|
|
4247
|
+
if (u !== false)
|
|
4248
|
+
(u == true ? runtime.UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
|
4249
|
+
return writer;
|
|
4250
|
+
}
|
|
4251
|
+
}
|
|
4252
|
+
/**
|
|
4253
|
+
* @generated MessageType for protobuf message stream.video.sfu.event.ParticipantMigrationComplete
|
|
4254
|
+
*/
|
|
4255
|
+
const ParticipantMigrationComplete = new ParticipantMigrationComplete$Type();
|
|
4256
|
+
// @generated message type with reflection information, may provide speed optimized methods
|
|
4211
4257
|
class PinsChanged$Type extends runtime.MessageType {
|
|
4212
4258
|
constructor() {
|
|
4213
4259
|
super('stream.video.sfu.event.PinsChanged', [
|
|
@@ -4458,6 +4504,13 @@ class SfuRequest$Type extends runtime.MessageType {
|
|
|
4458
4504
|
oneof: 'requestPayload',
|
|
4459
4505
|
T: () => HealthCheckRequest,
|
|
4460
4506
|
},
|
|
4507
|
+
{
|
|
4508
|
+
no: 3,
|
|
4509
|
+
name: 'leave_call_request',
|
|
4510
|
+
kind: 'message',
|
|
4511
|
+
oneof: 'requestPayload',
|
|
4512
|
+
T: () => LeaveCallRequest,
|
|
4513
|
+
},
|
|
4461
4514
|
]);
|
|
4462
4515
|
}
|
|
4463
4516
|
create(value) {
|
|
@@ -4484,6 +4537,12 @@ class SfuRequest$Type extends runtime.MessageType {
|
|
|
4484
4537
|
healthCheckRequest: HealthCheckRequest.internalBinaryRead(reader, reader.uint32(), options, message.requestPayload.healthCheckRequest),
|
|
4485
4538
|
};
|
|
4486
4539
|
break;
|
|
4540
|
+
case /* stream.video.sfu.event.LeaveCallRequest leave_call_request */ 3:
|
|
4541
|
+
message.requestPayload = {
|
|
4542
|
+
oneofKind: 'leaveCallRequest',
|
|
4543
|
+
leaveCallRequest: LeaveCallRequest.internalBinaryRead(reader, reader.uint32(), options, message.requestPayload.leaveCallRequest),
|
|
4544
|
+
};
|
|
4545
|
+
break;
|
|
4487
4546
|
default:
|
|
4488
4547
|
let u = options.readUnknownField;
|
|
4489
4548
|
if (u === 'throw')
|
|
@@ -4502,6 +4561,9 @@ class SfuRequest$Type extends runtime.MessageType {
|
|
|
4502
4561
|
/* stream.video.sfu.event.HealthCheckRequest health_check_request = 2; */
|
|
4503
4562
|
if (message.requestPayload.oneofKind === 'healthCheckRequest')
|
|
4504
4563
|
HealthCheckRequest.internalBinaryWrite(message.requestPayload.healthCheckRequest, writer.tag(2, runtime.WireType.LengthDelimited).fork(), options).join();
|
|
4564
|
+
/* stream.video.sfu.event.LeaveCallRequest leave_call_request = 3; */
|
|
4565
|
+
if (message.requestPayload.oneofKind === 'leaveCallRequest')
|
|
4566
|
+
LeaveCallRequest.internalBinaryWrite(message.requestPayload.leaveCallRequest, writer.tag(3, runtime.WireType.LengthDelimited).fork(), options).join();
|
|
4505
4567
|
let u = options.writeUnknownFields;
|
|
4506
4568
|
if (u !== false)
|
|
4507
4569
|
(u == true ? runtime.UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
|
@@ -4513,19 +4575,74 @@ class SfuRequest$Type extends runtime.MessageType {
|
|
|
4513
4575
|
*/
|
|
4514
4576
|
const SfuRequest = new SfuRequest$Type();
|
|
4515
4577
|
// @generated message type with reflection information, may provide speed optimized methods
|
|
4516
|
-
class
|
|
4578
|
+
class LeaveCallRequest$Type extends runtime.MessageType {
|
|
4517
4579
|
constructor() {
|
|
4518
|
-
super('stream.video.sfu.event.
|
|
4580
|
+
super('stream.video.sfu.event.LeaveCallRequest', [
|
|
4581
|
+
{ no: 1, name: 'session_id', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
|
|
4582
|
+
{ no: 2, name: 'reason', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
|
|
4583
|
+
]);
|
|
4519
4584
|
}
|
|
4520
4585
|
create(value) {
|
|
4521
4586
|
const message = globalThis.Object.create(this.messagePrototype);
|
|
4587
|
+
message.sessionId = '';
|
|
4588
|
+
message.reason = '';
|
|
4522
4589
|
if (value !== undefined)
|
|
4523
4590
|
runtime.reflectionMergePartial(this, message, value);
|
|
4524
4591
|
return message;
|
|
4525
4592
|
}
|
|
4526
4593
|
internalBinaryRead(reader, length, options, target) {
|
|
4527
|
-
|
|
4528
|
-
|
|
4594
|
+
let message = target ?? this.create(), end = reader.pos + length;
|
|
4595
|
+
while (reader.pos < end) {
|
|
4596
|
+
let [fieldNo, wireType] = reader.tag();
|
|
4597
|
+
switch (fieldNo) {
|
|
4598
|
+
case /* string session_id */ 1:
|
|
4599
|
+
message.sessionId = reader.string();
|
|
4600
|
+
break;
|
|
4601
|
+
case /* string reason */ 2:
|
|
4602
|
+
message.reason = reader.string();
|
|
4603
|
+
break;
|
|
4604
|
+
default:
|
|
4605
|
+
let u = options.readUnknownField;
|
|
4606
|
+
if (u === 'throw')
|
|
4607
|
+
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
|
4608
|
+
let d = reader.skip(wireType);
|
|
4609
|
+
if (u !== false)
|
|
4610
|
+
(u === true ? runtime.UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
|
4611
|
+
}
|
|
4612
|
+
}
|
|
4613
|
+
return message;
|
|
4614
|
+
}
|
|
4615
|
+
internalBinaryWrite(message, writer, options) {
|
|
4616
|
+
/* string session_id = 1; */
|
|
4617
|
+
if (message.sessionId !== '')
|
|
4618
|
+
writer.tag(1, runtime.WireType.LengthDelimited).string(message.sessionId);
|
|
4619
|
+
/* string reason = 2; */
|
|
4620
|
+
if (message.reason !== '')
|
|
4621
|
+
writer.tag(2, runtime.WireType.LengthDelimited).string(message.reason);
|
|
4622
|
+
let u = options.writeUnknownFields;
|
|
4623
|
+
if (u !== false)
|
|
4624
|
+
(u == true ? runtime.UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
|
4625
|
+
return writer;
|
|
4626
|
+
}
|
|
4627
|
+
}
|
|
4628
|
+
/**
|
|
4629
|
+
* @generated MessageType for protobuf message stream.video.sfu.event.LeaveCallRequest
|
|
4630
|
+
*/
|
|
4631
|
+
const LeaveCallRequest = new LeaveCallRequest$Type();
|
|
4632
|
+
// @generated message type with reflection information, may provide speed optimized methods
|
|
4633
|
+
class HealthCheckRequest$Type extends runtime.MessageType {
|
|
4634
|
+
constructor() {
|
|
4635
|
+
super('stream.video.sfu.event.HealthCheckRequest', []);
|
|
4636
|
+
}
|
|
4637
|
+
create(value) {
|
|
4638
|
+
const message = globalThis.Object.create(this.messagePrototype);
|
|
4639
|
+
if (value !== undefined)
|
|
4640
|
+
runtime.reflectionMergePartial(this, message, value);
|
|
4641
|
+
return message;
|
|
4642
|
+
}
|
|
4643
|
+
internalBinaryRead(reader, length, options, target) {
|
|
4644
|
+
return target ?? this.create();
|
|
4645
|
+
}
|
|
4529
4646
|
internalBinaryWrite(message, writer, options) {
|
|
4530
4647
|
let u = options.writeUnknownFields;
|
|
4531
4648
|
if (u !== false)
|
|
@@ -4788,6 +4905,12 @@ class JoinRequest$Type extends runtime.MessageType {
|
|
|
4788
4905
|
kind: 'scalar',
|
|
4789
4906
|
T: 8 /*ScalarType.BOOL*/,
|
|
4790
4907
|
},
|
|
4908
|
+
{
|
|
4909
|
+
no: 7,
|
|
4910
|
+
name: 'reconnect_details',
|
|
4911
|
+
kind: 'message',
|
|
4912
|
+
T: () => ReconnectDetails,
|
|
4913
|
+
},
|
|
4791
4914
|
]);
|
|
4792
4915
|
}
|
|
4793
4916
|
create(value) {
|
|
@@ -4817,12 +4940,15 @@ class JoinRequest$Type extends runtime.MessageType {
|
|
|
4817
4940
|
case /* stream.video.sfu.models.ClientDetails client_details */ 4:
|
|
4818
4941
|
message.clientDetails = ClientDetails.internalBinaryRead(reader, reader.uint32(), options, message.clientDetails);
|
|
4819
4942
|
break;
|
|
4820
|
-
case /* stream.video.sfu.event.Migration migration
|
|
4943
|
+
case /* stream.video.sfu.event.Migration migration = 5 [deprecated = true];*/ 5:
|
|
4821
4944
|
message.migration = Migration.internalBinaryRead(reader, reader.uint32(), options, message.migration);
|
|
4822
4945
|
break;
|
|
4823
|
-
case /* bool fast_reconnect
|
|
4946
|
+
case /* bool fast_reconnect = 6 [deprecated = true];*/ 6:
|
|
4824
4947
|
message.fastReconnect = reader.bool();
|
|
4825
4948
|
break;
|
|
4949
|
+
case /* stream.video.sfu.event.ReconnectDetails reconnect_details */ 7:
|
|
4950
|
+
message.reconnectDetails = ReconnectDetails.internalBinaryRead(reader, reader.uint32(), options, message.reconnectDetails);
|
|
4951
|
+
break;
|
|
4826
4952
|
default:
|
|
4827
4953
|
let u = options.readUnknownField;
|
|
4828
4954
|
if (u === 'throw')
|
|
@@ -4847,12 +4973,15 @@ class JoinRequest$Type extends runtime.MessageType {
|
|
|
4847
4973
|
/* stream.video.sfu.models.ClientDetails client_details = 4; */
|
|
4848
4974
|
if (message.clientDetails)
|
|
4849
4975
|
ClientDetails.internalBinaryWrite(message.clientDetails, writer.tag(4, runtime.WireType.LengthDelimited).fork(), options).join();
|
|
4850
|
-
/* stream.video.sfu.event.Migration migration = 5; */
|
|
4976
|
+
/* stream.video.sfu.event.Migration migration = 5 [deprecated = true]; */
|
|
4851
4977
|
if (message.migration)
|
|
4852
4978
|
Migration.internalBinaryWrite(message.migration, writer.tag(5, runtime.WireType.LengthDelimited).fork(), options).join();
|
|
4853
|
-
/* bool fast_reconnect = 6; */
|
|
4979
|
+
/* bool fast_reconnect = 6 [deprecated = true]; */
|
|
4854
4980
|
if (message.fastReconnect !== false)
|
|
4855
4981
|
writer.tag(6, runtime.WireType.Varint).bool(message.fastReconnect);
|
|
4982
|
+
/* stream.video.sfu.event.ReconnectDetails reconnect_details = 7; */
|
|
4983
|
+
if (message.reconnectDetails)
|
|
4984
|
+
ReconnectDetails.internalBinaryWrite(message.reconnectDetails, writer.tag(7, runtime.WireType.LengthDelimited).fork(), options).join();
|
|
4856
4985
|
let u = options.writeUnknownFields;
|
|
4857
4986
|
if (u !== false)
|
|
4858
4987
|
(u == true ? runtime.UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
|
@@ -4864,6 +4993,129 @@ class JoinRequest$Type extends runtime.MessageType {
|
|
|
4864
4993
|
*/
|
|
4865
4994
|
const JoinRequest = new JoinRequest$Type();
|
|
4866
4995
|
// @generated message type with reflection information, may provide speed optimized methods
|
|
4996
|
+
class ReconnectDetails$Type extends runtime.MessageType {
|
|
4997
|
+
constructor() {
|
|
4998
|
+
super('stream.video.sfu.event.ReconnectDetails', [
|
|
4999
|
+
{
|
|
5000
|
+
no: 1,
|
|
5001
|
+
name: 'strategy',
|
|
5002
|
+
kind: 'enum',
|
|
5003
|
+
T: () => [
|
|
5004
|
+
'stream.video.sfu.models.WebsocketReconnectStrategy',
|
|
5005
|
+
WebsocketReconnectStrategy,
|
|
5006
|
+
'WEBSOCKET_RECONNECT_STRATEGY_',
|
|
5007
|
+
],
|
|
5008
|
+
},
|
|
5009
|
+
{
|
|
5010
|
+
no: 3,
|
|
5011
|
+
name: 'announced_tracks',
|
|
5012
|
+
kind: 'message',
|
|
5013
|
+
repeat: 1 /*RepeatType.PACKED*/,
|
|
5014
|
+
T: () => TrackInfo,
|
|
5015
|
+
},
|
|
5016
|
+
{
|
|
5017
|
+
no: 4,
|
|
5018
|
+
name: 'subscriptions',
|
|
5019
|
+
kind: 'message',
|
|
5020
|
+
repeat: 1 /*RepeatType.PACKED*/,
|
|
5021
|
+
T: () => TrackSubscriptionDetails,
|
|
5022
|
+
},
|
|
5023
|
+
{
|
|
5024
|
+
no: 5,
|
|
5025
|
+
name: 'reconnect_attempt',
|
|
5026
|
+
kind: 'scalar',
|
|
5027
|
+
T: 13 /*ScalarType.UINT32*/,
|
|
5028
|
+
},
|
|
5029
|
+
{
|
|
5030
|
+
no: 6,
|
|
5031
|
+
name: 'from_sfu_id',
|
|
5032
|
+
kind: 'scalar',
|
|
5033
|
+
T: 9 /*ScalarType.STRING*/,
|
|
5034
|
+
},
|
|
5035
|
+
{
|
|
5036
|
+
no: 7,
|
|
5037
|
+
name: 'previous_session_id',
|
|
5038
|
+
kind: 'scalar',
|
|
5039
|
+
T: 9 /*ScalarType.STRING*/,
|
|
5040
|
+
},
|
|
5041
|
+
]);
|
|
5042
|
+
}
|
|
5043
|
+
create(value) {
|
|
5044
|
+
const message = globalThis.Object.create(this.messagePrototype);
|
|
5045
|
+
message.strategy = 0;
|
|
5046
|
+
message.announcedTracks = [];
|
|
5047
|
+
message.subscriptions = [];
|
|
5048
|
+
message.reconnectAttempt = 0;
|
|
5049
|
+
message.fromSfuId = '';
|
|
5050
|
+
message.previousSessionId = '';
|
|
5051
|
+
if (value !== undefined)
|
|
5052
|
+
runtime.reflectionMergePartial(this, message, value);
|
|
5053
|
+
return message;
|
|
5054
|
+
}
|
|
5055
|
+
internalBinaryRead(reader, length, options, target) {
|
|
5056
|
+
let message = target ?? this.create(), end = reader.pos + length;
|
|
5057
|
+
while (reader.pos < end) {
|
|
5058
|
+
let [fieldNo, wireType] = reader.tag();
|
|
5059
|
+
switch (fieldNo) {
|
|
5060
|
+
case /* stream.video.sfu.models.WebsocketReconnectStrategy strategy */ 1:
|
|
5061
|
+
message.strategy = reader.int32();
|
|
5062
|
+
break;
|
|
5063
|
+
case /* repeated stream.video.sfu.models.TrackInfo announced_tracks */ 3:
|
|
5064
|
+
message.announcedTracks.push(TrackInfo.internalBinaryRead(reader, reader.uint32(), options));
|
|
5065
|
+
break;
|
|
5066
|
+
case /* repeated stream.video.sfu.signal.TrackSubscriptionDetails subscriptions */ 4:
|
|
5067
|
+
message.subscriptions.push(TrackSubscriptionDetails.internalBinaryRead(reader, reader.uint32(), options));
|
|
5068
|
+
break;
|
|
5069
|
+
case /* uint32 reconnect_attempt */ 5:
|
|
5070
|
+
message.reconnectAttempt = reader.uint32();
|
|
5071
|
+
break;
|
|
5072
|
+
case /* string from_sfu_id */ 6:
|
|
5073
|
+
message.fromSfuId = reader.string();
|
|
5074
|
+
break;
|
|
5075
|
+
case /* string previous_session_id */ 7:
|
|
5076
|
+
message.previousSessionId = reader.string();
|
|
5077
|
+
break;
|
|
5078
|
+
default:
|
|
5079
|
+
let u = options.readUnknownField;
|
|
5080
|
+
if (u === 'throw')
|
|
5081
|
+
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
|
5082
|
+
let d = reader.skip(wireType);
|
|
5083
|
+
if (u !== false)
|
|
5084
|
+
(u === true ? runtime.UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
|
5085
|
+
}
|
|
5086
|
+
}
|
|
5087
|
+
return message;
|
|
5088
|
+
}
|
|
5089
|
+
internalBinaryWrite(message, writer, options) {
|
|
5090
|
+
/* stream.video.sfu.models.WebsocketReconnectStrategy strategy = 1; */
|
|
5091
|
+
if (message.strategy !== 0)
|
|
5092
|
+
writer.tag(1, runtime.WireType.Varint).int32(message.strategy);
|
|
5093
|
+
/* repeated stream.video.sfu.models.TrackInfo announced_tracks = 3; */
|
|
5094
|
+
for (let i = 0; i < message.announcedTracks.length; i++)
|
|
5095
|
+
TrackInfo.internalBinaryWrite(message.announcedTracks[i], writer.tag(3, runtime.WireType.LengthDelimited).fork(), options).join();
|
|
5096
|
+
/* repeated stream.video.sfu.signal.TrackSubscriptionDetails subscriptions = 4; */
|
|
5097
|
+
for (let i = 0; i < message.subscriptions.length; i++)
|
|
5098
|
+
TrackSubscriptionDetails.internalBinaryWrite(message.subscriptions[i], writer.tag(4, runtime.WireType.LengthDelimited).fork(), options).join();
|
|
5099
|
+
/* uint32 reconnect_attempt = 5; */
|
|
5100
|
+
if (message.reconnectAttempt !== 0)
|
|
5101
|
+
writer.tag(5, runtime.WireType.Varint).uint32(message.reconnectAttempt);
|
|
5102
|
+
/* string from_sfu_id = 6; */
|
|
5103
|
+
if (message.fromSfuId !== '')
|
|
5104
|
+
writer.tag(6, runtime.WireType.LengthDelimited).string(message.fromSfuId);
|
|
5105
|
+
/* string previous_session_id = 7; */
|
|
5106
|
+
if (message.previousSessionId !== '')
|
|
5107
|
+
writer.tag(7, runtime.WireType.LengthDelimited).string(message.previousSessionId);
|
|
5108
|
+
let u = options.writeUnknownFields;
|
|
5109
|
+
if (u !== false)
|
|
5110
|
+
(u == true ? runtime.UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
|
5111
|
+
return writer;
|
|
5112
|
+
}
|
|
5113
|
+
}
|
|
5114
|
+
/**
|
|
5115
|
+
* @generated MessageType for protobuf message stream.video.sfu.event.ReconnectDetails
|
|
5116
|
+
*/
|
|
5117
|
+
const ReconnectDetails = new ReconnectDetails$Type();
|
|
5118
|
+
// @generated message type with reflection information, may provide speed optimized methods
|
|
4867
5119
|
class Migration$Type extends runtime.MessageType {
|
|
4868
5120
|
constructor() {
|
|
4869
5121
|
super('stream.video.sfu.event.Migration', [
|
|
@@ -4949,11 +5201,18 @@ class JoinResponse$Type extends runtime.MessageType {
|
|
|
4949
5201
|
super('stream.video.sfu.event.JoinResponse', [
|
|
4950
5202
|
{ no: 1, name: 'call_state', kind: 'message', T: () => CallState$1 },
|
|
4951
5203
|
{ no: 2, name: 'reconnected', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
|
|
5204
|
+
{
|
|
5205
|
+
no: 3,
|
|
5206
|
+
name: 'fast_reconnect_deadline_seconds',
|
|
5207
|
+
kind: 'scalar',
|
|
5208
|
+
T: 5 /*ScalarType.INT32*/,
|
|
5209
|
+
},
|
|
4952
5210
|
]);
|
|
4953
5211
|
}
|
|
4954
5212
|
create(value) {
|
|
4955
5213
|
const message = globalThis.Object.create(this.messagePrototype);
|
|
4956
5214
|
message.reconnected = false;
|
|
5215
|
+
message.fastReconnectDeadlineSeconds = 0;
|
|
4957
5216
|
if (value !== undefined)
|
|
4958
5217
|
runtime.reflectionMergePartial(this, message, value);
|
|
4959
5218
|
return message;
|
|
@@ -4969,6 +5228,9 @@ class JoinResponse$Type extends runtime.MessageType {
|
|
|
4969
5228
|
case /* bool reconnected */ 2:
|
|
4970
5229
|
message.reconnected = reader.bool();
|
|
4971
5230
|
break;
|
|
5231
|
+
case /* int32 fast_reconnect_deadline_seconds */ 3:
|
|
5232
|
+
message.fastReconnectDeadlineSeconds = reader.int32();
|
|
5233
|
+
break;
|
|
4972
5234
|
default:
|
|
4973
5235
|
let u = options.readUnknownField;
|
|
4974
5236
|
if (u === 'throw')
|
|
@@ -4987,6 +5249,11 @@ class JoinResponse$Type extends runtime.MessageType {
|
|
|
4987
5249
|
/* bool reconnected = 2; */
|
|
4988
5250
|
if (message.reconnected !== false)
|
|
4989
5251
|
writer.tag(2, runtime.WireType.Varint).bool(message.reconnected);
|
|
5252
|
+
/* int32 fast_reconnect_deadline_seconds = 3; */
|
|
5253
|
+
if (message.fastReconnectDeadlineSeconds !== 0)
|
|
5254
|
+
writer
|
|
5255
|
+
.tag(3, runtime.WireType.Varint)
|
|
5256
|
+
.int32(message.fastReconnectDeadlineSeconds);
|
|
4990
5257
|
let u = options.writeUnknownFields;
|
|
4991
5258
|
if (u !== false)
|
|
4992
5259
|
(u == true ? runtime.UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
|
@@ -6187,12 +6454,15 @@ var events = /*#__PURE__*/Object.freeze({
|
|
|
6187
6454
|
ICETrickle: ICETrickle,
|
|
6188
6455
|
JoinRequest: JoinRequest,
|
|
6189
6456
|
JoinResponse: JoinResponse,
|
|
6457
|
+
LeaveCallRequest: LeaveCallRequest,
|
|
6190
6458
|
Migration: Migration,
|
|
6191
6459
|
ParticipantJoined: ParticipantJoined,
|
|
6192
6460
|
ParticipantLeft: ParticipantLeft,
|
|
6461
|
+
ParticipantMigrationComplete: ParticipantMigrationComplete,
|
|
6193
6462
|
ParticipantUpdated: ParticipantUpdated,
|
|
6194
6463
|
PinsChanged: PinsChanged,
|
|
6195
6464
|
PublisherAnswer: PublisherAnswer,
|
|
6465
|
+
ReconnectDetails: ReconnectDetails,
|
|
6196
6466
|
SfuEvent: SfuEvent,
|
|
6197
6467
|
SfuRequest: SfuRequest,
|
|
6198
6468
|
SubscriberOffer: SubscriberOffer,
|
|
@@ -6318,6 +6588,17 @@ const withHeaders = (headers) => {
|
|
|
6318
6588
|
},
|
|
6319
6589
|
};
|
|
6320
6590
|
};
|
|
6591
|
+
const withRequestLogger = (logger, level) => {
|
|
6592
|
+
return {
|
|
6593
|
+
interceptUnary: (next, method, input, options) => {
|
|
6594
|
+
logger(level, `Calling SFU RPC method ${method.name}`, {
|
|
6595
|
+
input,
|
|
6596
|
+
options,
|
|
6597
|
+
});
|
|
6598
|
+
return next(method, input, options);
|
|
6599
|
+
},
|
|
6600
|
+
};
|
|
6601
|
+
};
|
|
6321
6602
|
/**
|
|
6322
6603
|
* Creates new SignalServerClient instance.
|
|
6323
6604
|
*
|
|
@@ -6331,68 +6612,196 @@ const createSignalClient = (options) => {
|
|
|
6331
6612
|
return new SignalServerClient(transport);
|
|
6332
6613
|
};
|
|
6333
6614
|
|
|
6615
|
+
const sleep = (m) => new Promise((r) => setTimeout(r, m));
|
|
6616
|
+
function isFunction(value) {
|
|
6617
|
+
return (value &&
|
|
6618
|
+
(Object.prototype.toString.call(value) === '[object Function]' ||
|
|
6619
|
+
'function' === typeof value ||
|
|
6620
|
+
value instanceof Function));
|
|
6621
|
+
}
|
|
6334
6622
|
/**
|
|
6335
|
-
*
|
|
6623
|
+
* A map of known error codes.
|
|
6336
6624
|
*/
|
|
6337
|
-
const
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
|
|
6625
|
+
const KnownCodes = {
|
|
6626
|
+
TOKEN_EXPIRED: 40,
|
|
6627
|
+
WS_CLOSED_SUCCESS: 1000,
|
|
6628
|
+
WS_CLOSED_ABRUPTLY: 1006,
|
|
6629
|
+
WS_POLICY_VIOLATION: 1008,
|
|
6341
6630
|
};
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
break;
|
|
6361
|
-
}
|
|
6362
|
-
logMethod = console.error;
|
|
6363
|
-
break;
|
|
6364
|
-
case 'warn':
|
|
6365
|
-
if (isReactNative()) {
|
|
6366
|
-
message = `WARN: ${message}`;
|
|
6367
|
-
logMethod = console.info;
|
|
6368
|
-
break;
|
|
6369
|
-
}
|
|
6370
|
-
logMethod = console.warn;
|
|
6371
|
-
break;
|
|
6372
|
-
case 'info':
|
|
6373
|
-
logMethod = console.info;
|
|
6374
|
-
break;
|
|
6375
|
-
case 'trace':
|
|
6376
|
-
logMethod = console.trace;
|
|
6377
|
-
break;
|
|
6378
|
-
default:
|
|
6379
|
-
logMethod = console.log;
|
|
6380
|
-
break;
|
|
6631
|
+
/**
|
|
6632
|
+
* retryInterval - A retry interval which increases acc to number of failures
|
|
6633
|
+
*
|
|
6634
|
+
* @return {number} Duration to wait in milliseconds
|
|
6635
|
+
*/
|
|
6636
|
+
function retryInterval(numberOfFailures) {
|
|
6637
|
+
// try to reconnect in 0.25-5 seconds (random to spread out the load from failures)
|
|
6638
|
+
const max = Math.min(500 + numberOfFailures * 2000, 5000);
|
|
6639
|
+
const min = Math.min(Math.max(250, (numberOfFailures - 1) * 2000), 5000);
|
|
6640
|
+
return Math.floor(Math.random() * (max - min) + min);
|
|
6641
|
+
}
|
|
6642
|
+
function randomId() {
|
|
6643
|
+
return generateUUIDv4();
|
|
6644
|
+
}
|
|
6645
|
+
function hex(bytes) {
|
|
6646
|
+
let s = '';
|
|
6647
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
6648
|
+
s += bytes[i].toString(16).padStart(2, '0');
|
|
6381
6649
|
}
|
|
6382
|
-
|
|
6383
|
-
}
|
|
6384
|
-
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
|
|
6650
|
+
return s;
|
|
6651
|
+
}
|
|
6652
|
+
// https://tools.ietf.org/html/rfc4122
|
|
6653
|
+
function generateUUIDv4() {
|
|
6654
|
+
const bytes = getRandomBytes(16);
|
|
6655
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40; // version
|
|
6656
|
+
bytes[8] = (bytes[8] & 0xbf) | 0x80; // variant
|
|
6657
|
+
return (hex(bytes.subarray(0, 4)) +
|
|
6658
|
+
'-' +
|
|
6659
|
+
hex(bytes.subarray(4, 6)) +
|
|
6660
|
+
'-' +
|
|
6661
|
+
hex(bytes.subarray(6, 8)) +
|
|
6662
|
+
'-' +
|
|
6663
|
+
hex(bytes.subarray(8, 10)) +
|
|
6664
|
+
'-' +
|
|
6665
|
+
hex(bytes.subarray(10, 16)));
|
|
6666
|
+
}
|
|
6667
|
+
function getRandomValuesWithMathRandom(bytes) {
|
|
6668
|
+
const max = Math.pow(2, (8 * bytes.byteLength) / bytes.length);
|
|
6669
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
6670
|
+
bytes[i] = Math.random() * max;
|
|
6388
6671
|
}
|
|
6389
|
-
}
|
|
6390
|
-
const
|
|
6391
|
-
|
|
6392
|
-
|
|
6672
|
+
}
|
|
6673
|
+
const getRandomValues = (() => {
|
|
6674
|
+
if (typeof crypto !== 'undefined' &&
|
|
6675
|
+
typeof crypto?.getRandomValues !== 'undefined') {
|
|
6676
|
+
return crypto.getRandomValues.bind(crypto);
|
|
6677
|
+
}
|
|
6678
|
+
else if (typeof msCrypto !== 'undefined') {
|
|
6679
|
+
return msCrypto.getRandomValues.bind(msCrypto);
|
|
6680
|
+
}
|
|
6681
|
+
else {
|
|
6682
|
+
return getRandomValuesWithMathRandom;
|
|
6683
|
+
}
|
|
6684
|
+
})();
|
|
6685
|
+
function getRandomBytes(length) {
|
|
6686
|
+
const bytes = new Uint8Array(length);
|
|
6687
|
+
getRandomValues(bytes);
|
|
6688
|
+
return bytes;
|
|
6689
|
+
}
|
|
6690
|
+
function convertErrorToJson(err) {
|
|
6691
|
+
const jsonObj = {};
|
|
6692
|
+
if (!err)
|
|
6693
|
+
return jsonObj;
|
|
6694
|
+
try {
|
|
6695
|
+
Object.getOwnPropertyNames(err).forEach((key) => {
|
|
6696
|
+
jsonObj[key] = Object.getOwnPropertyDescriptor(err, key);
|
|
6697
|
+
});
|
|
6698
|
+
}
|
|
6699
|
+
catch (_) {
|
|
6700
|
+
return {
|
|
6701
|
+
error: 'failed to serialize the error',
|
|
6702
|
+
};
|
|
6703
|
+
}
|
|
6704
|
+
return jsonObj;
|
|
6705
|
+
}
|
|
6706
|
+
/**
|
|
6707
|
+
* isOnline safely return the navigator.online value for browser env
|
|
6708
|
+
* if navigator is not in global object, it always return true
|
|
6709
|
+
*/
|
|
6710
|
+
function isOnline(logger) {
|
|
6711
|
+
const nav = typeof navigator !== 'undefined'
|
|
6712
|
+
? navigator
|
|
6713
|
+
: typeof window !== 'undefined' && window.navigator
|
|
6714
|
+
? window.navigator
|
|
6715
|
+
: undefined;
|
|
6716
|
+
if (!nav) {
|
|
6717
|
+
logger('warn', 'isOnline failed to access window.navigator and assume browser is online');
|
|
6718
|
+
return true;
|
|
6719
|
+
}
|
|
6720
|
+
// RN navigator has undefined for onLine
|
|
6721
|
+
if (typeof nav.onLine !== 'boolean') {
|
|
6722
|
+
return true;
|
|
6723
|
+
}
|
|
6724
|
+
return nav.onLine;
|
|
6725
|
+
}
|
|
6726
|
+
/**
|
|
6727
|
+
* listenForConnectionChanges - Adds an event listener fired on browser going online or offline
|
|
6728
|
+
*/
|
|
6729
|
+
function addConnectionEventListeners(cb) {
|
|
6730
|
+
if (typeof window !== 'undefined' && window.addEventListener) {
|
|
6731
|
+
window.addEventListener('offline', cb);
|
|
6732
|
+
window.addEventListener('online', cb);
|
|
6733
|
+
}
|
|
6734
|
+
}
|
|
6735
|
+
function removeConnectionEventListeners(cb) {
|
|
6736
|
+
if (typeof window !== 'undefined' && window.removeEventListener) {
|
|
6737
|
+
window.removeEventListener('offline', cb);
|
|
6738
|
+
window.removeEventListener('online', cb);
|
|
6739
|
+
}
|
|
6740
|
+
}
|
|
6741
|
+
|
|
6742
|
+
/**
|
|
6743
|
+
* Checks whether we are using React Native
|
|
6744
|
+
*/
|
|
6745
|
+
const isReactNative = () => {
|
|
6746
|
+
if (typeof navigator === 'undefined')
|
|
6747
|
+
return false;
|
|
6748
|
+
return navigator.product?.toLowerCase() === 'reactnative';
|
|
6749
|
+
};
|
|
6750
|
+
|
|
6751
|
+
// log levels, sorted by verbosity
|
|
6752
|
+
const logLevels = Object.freeze({
|
|
6753
|
+
trace: 0,
|
|
6754
|
+
debug: 1,
|
|
6755
|
+
info: 2,
|
|
6756
|
+
warn: 3,
|
|
6757
|
+
error: 4,
|
|
6758
|
+
});
|
|
6759
|
+
let logger$2;
|
|
6760
|
+
let level = 'info';
|
|
6761
|
+
const logToConsole = (logLevel, message, ...args) => {
|
|
6762
|
+
let logMethod;
|
|
6763
|
+
switch (logLevel) {
|
|
6764
|
+
case 'error':
|
|
6765
|
+
if (isReactNative()) {
|
|
6766
|
+
message = `ERROR: ${message}`;
|
|
6767
|
+
logMethod = console.info;
|
|
6768
|
+
break;
|
|
6769
|
+
}
|
|
6770
|
+
logMethod = console.error;
|
|
6771
|
+
break;
|
|
6772
|
+
case 'warn':
|
|
6773
|
+
if (isReactNative()) {
|
|
6774
|
+
message = `WARN: ${message}`;
|
|
6775
|
+
logMethod = console.info;
|
|
6776
|
+
break;
|
|
6777
|
+
}
|
|
6778
|
+
logMethod = console.warn;
|
|
6779
|
+
break;
|
|
6780
|
+
case 'info':
|
|
6781
|
+
logMethod = console.info;
|
|
6782
|
+
break;
|
|
6783
|
+
case 'trace':
|
|
6784
|
+
logMethod = console.trace;
|
|
6785
|
+
break;
|
|
6786
|
+
default:
|
|
6787
|
+
logMethod = console.log;
|
|
6788
|
+
break;
|
|
6789
|
+
}
|
|
6790
|
+
logMethod(message, ...args);
|
|
6791
|
+
};
|
|
6792
|
+
const setLogger = (l, lvl) => {
|
|
6793
|
+
logger$2 = l;
|
|
6794
|
+
if (lvl) {
|
|
6795
|
+
setLogLevel(lvl);
|
|
6796
|
+
}
|
|
6797
|
+
};
|
|
6798
|
+
const setLogLevel = (l) => {
|
|
6799
|
+
level = l;
|
|
6800
|
+
};
|
|
6801
|
+
const getLogLevel = () => level;
|
|
6393
6802
|
const getLogger = (withTags) => {
|
|
6394
|
-
const loggerMethod = logger$
|
|
6395
|
-
const tags = (withTags || []).join(':');
|
|
6803
|
+
const loggerMethod = logger$2 || logToConsole;
|
|
6804
|
+
const tags = (withTags || []).filter(Boolean).join(':');
|
|
6396
6805
|
const result = (logLevel, message, ...args) => {
|
|
6397
6806
|
if (logLevels[logLevel] >= logLevels[level]) {
|
|
6398
6807
|
loggerMethod(logLevel, `[${tags}]: ${message}`, ...args);
|
|
@@ -6401,6 +6810,37 @@ const getLogger = (withTags) => {
|
|
|
6401
6810
|
return result;
|
|
6402
6811
|
};
|
|
6403
6812
|
|
|
6813
|
+
/**
|
|
6814
|
+
* Creates a closure which wraps the given RPC call and retries invoking
|
|
6815
|
+
* the RPC until it succeeds or the maximum number of retries is reached.
|
|
6816
|
+
*
|
|
6817
|
+
* For each retry, there would be a delay to avoid request bursts toward the SFU.
|
|
6818
|
+
*
|
|
6819
|
+
* @param rpc the closure around the RPC call to execute.
|
|
6820
|
+
* @param signal the signal to abort the RPC call and retries loop.
|
|
6821
|
+
*/
|
|
6822
|
+
const retryable = async (rpc, signal) => {
|
|
6823
|
+
let attempt = 0;
|
|
6824
|
+
let result = undefined;
|
|
6825
|
+
do {
|
|
6826
|
+
if (attempt > 0)
|
|
6827
|
+
await sleep(retryInterval(attempt));
|
|
6828
|
+
try {
|
|
6829
|
+
result = await rpc();
|
|
6830
|
+
}
|
|
6831
|
+
catch (err) {
|
|
6832
|
+
const isRequestCancelled = err instanceof runtimeRpc.RpcError &&
|
|
6833
|
+
err.code === twirpTransport.TwirpErrorCode[twirpTransport.TwirpErrorCode.cancelled];
|
|
6834
|
+
const isAborted = signal?.aborted ?? false;
|
|
6835
|
+
if (isRequestCancelled || isAborted)
|
|
6836
|
+
throw err;
|
|
6837
|
+
getLogger(['sfu-client', 'rpc'])('debug', `rpc failed (${attempt})`, err);
|
|
6838
|
+
attempt++;
|
|
6839
|
+
}
|
|
6840
|
+
} while (!result || result.response.error?.shouldRetry);
|
|
6841
|
+
return result;
|
|
6842
|
+
};
|
|
6843
|
+
|
|
6404
6844
|
const getPreferredCodecs = (kind, preferredCodec, codecToRemove) => {
|
|
6405
6845
|
const logger = getLogger(['codecs']);
|
|
6406
6846
|
if (!('getCapabilities' in RTCRtpReceiver)) {
|
|
@@ -6473,6 +6913,7 @@ const sfuEventKinds = {
|
|
|
6473
6913
|
pinsUpdated: undefined,
|
|
6474
6914
|
callEnded: undefined,
|
|
6475
6915
|
participantUpdated: undefined,
|
|
6916
|
+
participantMigrationComplete: undefined,
|
|
6476
6917
|
};
|
|
6477
6918
|
const isSfuEvent = (eventName) => {
|
|
6478
6919
|
return Object.prototype.hasOwnProperty.call(sfuEventKinds, eventName);
|
|
@@ -6481,12 +6922,12 @@ class Dispatcher {
|
|
|
6481
6922
|
constructor() {
|
|
6482
6923
|
this.logger = getLogger(['Dispatcher']);
|
|
6483
6924
|
this.subscribers = {};
|
|
6484
|
-
this.dispatch = (message) => {
|
|
6925
|
+
this.dispatch = (message, logTag) => {
|
|
6485
6926
|
const eventKind = message.eventPayload.oneofKind;
|
|
6486
6927
|
if (!eventKind)
|
|
6487
6928
|
return;
|
|
6488
6929
|
const payload = message.eventPayload[eventKind];
|
|
6489
|
-
this.logger('debug', `Dispatching ${eventKind}`, payload);
|
|
6930
|
+
this.logger('debug', `Dispatching ${eventKind}, tag=${logTag}`, payload);
|
|
6490
6931
|
const listeners = this.subscribers[eventKind];
|
|
6491
6932
|
if (!listeners)
|
|
6492
6933
|
return;
|
|
@@ -6528,7 +6969,6 @@ class IceTrickleBuffer {
|
|
|
6528
6969
|
constructor() {
|
|
6529
6970
|
this.subscriberCandidates = new rxjs.ReplaySubject();
|
|
6530
6971
|
this.publisherCandidates = new rxjs.ReplaySubject();
|
|
6531
|
-
this.logger = getLogger(['sfu-client']);
|
|
6532
6972
|
this.push = (iceTrickle) => {
|
|
6533
6973
|
if (iceTrickle.peerType === PeerType.SUBSCRIBER) {
|
|
6534
6974
|
this.subscriberCandidates.next(iceTrickle);
|
|
@@ -6537,7 +6977,8 @@ class IceTrickleBuffer {
|
|
|
6537
6977
|
this.publisherCandidates.next(iceTrickle);
|
|
6538
6978
|
}
|
|
6539
6979
|
else {
|
|
6540
|
-
|
|
6980
|
+
const logger = getLogger(['sfu-client']);
|
|
6981
|
+
logger('warn', `ICETrickle, Unknown peer type`, iceTrickle);
|
|
6541
6982
|
}
|
|
6542
6983
|
};
|
|
6543
6984
|
}
|
|
@@ -6556,72 +6997,6 @@ function getIceCandidate(candidate) {
|
|
|
6556
6997
|
}
|
|
6557
6998
|
}
|
|
6558
6999
|
|
|
6559
|
-
const version = "1.5.0" ;
|
|
6560
|
-
const [major, minor, patch] = version.split('.');
|
|
6561
|
-
let sdkInfo = {
|
|
6562
|
-
type: SdkType.PLAIN_JAVASCRIPT,
|
|
6563
|
-
major,
|
|
6564
|
-
minor,
|
|
6565
|
-
patch,
|
|
6566
|
-
};
|
|
6567
|
-
let osInfo;
|
|
6568
|
-
let deviceInfo;
|
|
6569
|
-
let webRtcInfo;
|
|
6570
|
-
const setSdkInfo = (info) => {
|
|
6571
|
-
sdkInfo = info;
|
|
6572
|
-
};
|
|
6573
|
-
const getSdkInfo = () => {
|
|
6574
|
-
return sdkInfo;
|
|
6575
|
-
};
|
|
6576
|
-
const setOSInfo = (info) => {
|
|
6577
|
-
osInfo = info;
|
|
6578
|
-
};
|
|
6579
|
-
const getOSInfo = () => {
|
|
6580
|
-
return osInfo;
|
|
6581
|
-
};
|
|
6582
|
-
const setDeviceInfo = (info) => {
|
|
6583
|
-
deviceInfo = info;
|
|
6584
|
-
};
|
|
6585
|
-
const getDeviceInfo = () => {
|
|
6586
|
-
return deviceInfo;
|
|
6587
|
-
};
|
|
6588
|
-
const getWebRTCInfo = () => {
|
|
6589
|
-
return webRtcInfo;
|
|
6590
|
-
};
|
|
6591
|
-
const setWebRTCInfo = (info) => {
|
|
6592
|
-
webRtcInfo = info;
|
|
6593
|
-
};
|
|
6594
|
-
const getClientDetails = () => {
|
|
6595
|
-
if (isReactNative()) {
|
|
6596
|
-
// Since RN doesn't support web, sharing browser info is not required
|
|
6597
|
-
return {
|
|
6598
|
-
sdk: getSdkInfo(),
|
|
6599
|
-
os: getOSInfo(),
|
|
6600
|
-
device: getDeviceInfo(),
|
|
6601
|
-
};
|
|
6602
|
-
}
|
|
6603
|
-
const userAgent = new uaParserJs.UAParser(navigator.userAgent);
|
|
6604
|
-
const { browser, os, device, cpu } = userAgent.getResult();
|
|
6605
|
-
return {
|
|
6606
|
-
sdk: getSdkInfo(),
|
|
6607
|
-
browser: {
|
|
6608
|
-
name: browser.name || navigator.userAgent,
|
|
6609
|
-
version: browser.version || '',
|
|
6610
|
-
},
|
|
6611
|
-
os: {
|
|
6612
|
-
name: os.name || '',
|
|
6613
|
-
version: os.version || '',
|
|
6614
|
-
architecture: cpu.architecture || '',
|
|
6615
|
-
},
|
|
6616
|
-
device: {
|
|
6617
|
-
name: [device.vendor, device.model, device.type]
|
|
6618
|
-
.filter(Boolean)
|
|
6619
|
-
.join(' '),
|
|
6620
|
-
version: '',
|
|
6621
|
-
},
|
|
6622
|
-
};
|
|
6623
|
-
};
|
|
6624
|
-
|
|
6625
7000
|
const DEFAULT_BITRATE = 1250000;
|
|
6626
7001
|
const defaultTargetResolution = {
|
|
6627
7002
|
bitrate: DEFAULT_BITRATE,
|
|
@@ -6644,7 +7019,6 @@ const findOptimalVideoLayers = (videoTrack, targetResolution = defaultTargetReso
|
|
|
6644
7019
|
const optimalVideoLayers = [];
|
|
6645
7020
|
const settings = videoTrack.getSettings();
|
|
6646
7021
|
const { width: w = 0, height: h = 0 } = settings;
|
|
6647
|
-
const isRNIos = isReactNative() && getOSInfo()?.name.toLowerCase() === 'ios';
|
|
6648
7022
|
const maxBitrate = getComputedMaxBitrate(targetResolution, w, h);
|
|
6649
7023
|
let downscaleFactor = 1;
|
|
6650
7024
|
['f', 'h', 'q'].forEach((rid) => {
|
|
@@ -6658,12 +7032,7 @@ const findOptimalVideoLayers = (videoTrack, targetResolution = defaultTargetReso
|
|
|
6658
7032
|
height: Math.round(h / downscaleFactor),
|
|
6659
7033
|
maxBitrate: Math.round(maxBitrate / downscaleFactor) || defaultBitratePerRid[rid],
|
|
6660
7034
|
scaleResolutionDownBy: downscaleFactor,
|
|
6661
|
-
|
|
6662
|
-
maxFramerate: {
|
|
6663
|
-
f: 30,
|
|
6664
|
-
h: isRNIos ? 30 : 25,
|
|
6665
|
-
q: isRNIos ? 30 : 20,
|
|
6666
|
-
}[rid],
|
|
7035
|
+
maxFramerate: 30,
|
|
6667
7036
|
});
|
|
6668
7037
|
downscaleFactor *= 2;
|
|
6669
7038
|
});
|
|
@@ -6738,6 +7107,10 @@ const findOptimalScreenSharingLayers = (videoTrack, preferences, defaultMaxBitra
|
|
|
6738
7107
|
];
|
|
6739
7108
|
};
|
|
6740
7109
|
|
|
7110
|
+
const ensureExhausted = (x, message) => {
|
|
7111
|
+
getLogger(['helpers'])('warn', message, x);
|
|
7112
|
+
};
|
|
7113
|
+
|
|
6741
7114
|
const trackTypeToParticipantStreamKey = (trackType) => {
|
|
6742
7115
|
switch (trackType) {
|
|
6743
7116
|
case TrackType.SCREEN_SHARE:
|
|
@@ -6751,8 +7124,7 @@ const trackTypeToParticipantStreamKey = (trackType) => {
|
|
|
6751
7124
|
case TrackType.UNSPECIFIED:
|
|
6752
7125
|
throw new Error('Track type is unspecified');
|
|
6753
7126
|
default:
|
|
6754
|
-
|
|
6755
|
-
throw new Error(`Unknown track type: ${exhaustiveTrackTypeCheck}`);
|
|
7127
|
+
ensureExhausted(trackType, 'Unknown track type');
|
|
6756
7128
|
}
|
|
6757
7129
|
};
|
|
6758
7130
|
const muteTypeToTrackType = (muteType) => {
|
|
@@ -6766,8 +7138,21 @@ const muteTypeToTrackType = (muteType) => {
|
|
|
6766
7138
|
case 'screenshare_audio':
|
|
6767
7139
|
return TrackType.SCREEN_SHARE_AUDIO;
|
|
6768
7140
|
default:
|
|
6769
|
-
|
|
6770
|
-
|
|
7141
|
+
ensureExhausted(muteType, 'Unknown mute type');
|
|
7142
|
+
}
|
|
7143
|
+
};
|
|
7144
|
+
const toTrackType = (trackType) => {
|
|
7145
|
+
switch (trackType) {
|
|
7146
|
+
case 'TRACK_TYPE_AUDIO':
|
|
7147
|
+
return TrackType.AUDIO;
|
|
7148
|
+
case 'TRACK_TYPE_VIDEO':
|
|
7149
|
+
return TrackType.VIDEO;
|
|
7150
|
+
case 'TRACK_TYPE_SCREEN_SHARE':
|
|
7151
|
+
return TrackType.SCREEN_SHARE;
|
|
7152
|
+
case 'TRACK_TYPE_SCREEN_SHARE_AUDIO':
|
|
7153
|
+
return TrackType.SCREEN_SHARE_AUDIO;
|
|
7154
|
+
default:
|
|
7155
|
+
return undefined;
|
|
6771
7156
|
}
|
|
6772
7157
|
};
|
|
6773
7158
|
|
|
@@ -7287,6 +7672,11 @@ class CallState {
|
|
|
7287
7672
|
this.anonymousParticipantCountSubject = new rxjs.BehaviorSubject(0);
|
|
7288
7673
|
this.participantsSubject = new rxjs.BehaviorSubject([]);
|
|
7289
7674
|
this.callStatsReportSubject = new rxjs.BehaviorSubject(undefined);
|
|
7675
|
+
// These are tracks that were delivered to the Subscriber's onTrack event
|
|
7676
|
+
// that we couldn't associate with a participant yet.
|
|
7677
|
+
// This happens when the participantJoined event hasn't been received yet.
|
|
7678
|
+
// We keep these tracks around until we can associate them with a participant.
|
|
7679
|
+
this.orphanedTracks = [];
|
|
7290
7680
|
this.logger = getLogger(['CallState']);
|
|
7291
7681
|
/**
|
|
7292
7682
|
* A list of comparators that are used to sort the participants.
|
|
@@ -7476,7 +7866,7 @@ class CallState {
|
|
|
7476
7866
|
*/
|
|
7477
7867
|
this.updateParticipants = (patch) => {
|
|
7478
7868
|
if (Object.keys(patch).length === 0)
|
|
7479
|
-
return;
|
|
7869
|
+
return this.participants;
|
|
7480
7870
|
return this.setParticipants((participants) => participants.map((p) => {
|
|
7481
7871
|
const thePatch = patch[p.sessionId];
|
|
7482
7872
|
if (thePatch) {
|
|
@@ -7535,6 +7925,41 @@ class CallState {
|
|
|
7535
7925
|
return participant;
|
|
7536
7926
|
}));
|
|
7537
7927
|
};
|
|
7928
|
+
/**
|
|
7929
|
+
* Adds an orphaned track to the call state.
|
|
7930
|
+
*
|
|
7931
|
+
* @internal
|
|
7932
|
+
*
|
|
7933
|
+
* @param orphanedTrack the orphaned track to add.
|
|
7934
|
+
*/
|
|
7935
|
+
this.registerOrphanedTrack = (orphanedTrack) => {
|
|
7936
|
+
this.orphanedTracks.push(orphanedTrack);
|
|
7937
|
+
};
|
|
7938
|
+
/**
|
|
7939
|
+
* Removes an orphaned track from the call state.
|
|
7940
|
+
*
|
|
7941
|
+
* @internal
|
|
7942
|
+
*
|
|
7943
|
+
* @param id the ID of the orphaned track to remove.
|
|
7944
|
+
*/
|
|
7945
|
+
this.removeOrphanedTrack = (id) => {
|
|
7946
|
+
this.orphanedTracks = this.orphanedTracks.filter((o) => o.id !== id);
|
|
7947
|
+
};
|
|
7948
|
+
/**
|
|
7949
|
+
* Takes all orphaned tracks with the given track lookup prefix.
|
|
7950
|
+
* All orphaned tracks with the given track lookup prefix are removed from the call state.
|
|
7951
|
+
*
|
|
7952
|
+
* @internal
|
|
7953
|
+
*
|
|
7954
|
+
* @param trackLookupPrefix the track lookup prefix to match the orphaned tracks by.
|
|
7955
|
+
*/
|
|
7956
|
+
this.takeOrphanedTracks = (trackLookupPrefix) => {
|
|
7957
|
+
const orphans = this.orphanedTracks.filter((orphan) => orphan.trackLookupPrefix === trackLookupPrefix);
|
|
7958
|
+
if (orphans.length > 0) {
|
|
7959
|
+
this.orphanedTracks = this.orphanedTracks.filter((orphan) => orphan.trackLookupPrefix !== trackLookupPrefix);
|
|
7960
|
+
}
|
|
7961
|
+
return orphans;
|
|
7962
|
+
};
|
|
7538
7963
|
/**
|
|
7539
7964
|
* Updates the call state with the data received from the server.
|
|
7540
7965
|
*
|
|
@@ -7559,6 +7984,43 @@ class CallState {
|
|
|
7559
7984
|
this.setCurrentValue(this.transcribingSubject, call.transcribing);
|
|
7560
7985
|
this.setCurrentValue(this.thumbnailsSubject, call.thumbnails);
|
|
7561
7986
|
};
|
|
7987
|
+
/**
|
|
7988
|
+
* Updates the call state with the data received from the SFU server.
|
|
7989
|
+
*
|
|
7990
|
+
* @internal
|
|
7991
|
+
*
|
|
7992
|
+
* @param callState the call state from the SFU server.
|
|
7993
|
+
* @param currentSessionId the session ID of the current user.
|
|
7994
|
+
* @param reconnectDetails optional reconnect details.
|
|
7995
|
+
*/
|
|
7996
|
+
this.updateFromSfuCallState = (callState, currentSessionId, reconnectDetails) => {
|
|
7997
|
+
const { participants, participantCount, startedAt, pins } = callState;
|
|
7998
|
+
const localPublishedTracks = reconnectDetails?.announcedTracks.map((t) => t.trackType) ?? [];
|
|
7999
|
+
this.setParticipants(() => {
|
|
8000
|
+
const participantLookup = this.getParticipantLookupBySessionId();
|
|
8001
|
+
return participants.map((p) => {
|
|
8002
|
+
// We need to preserve the local state of the participant
|
|
8003
|
+
// (e.g. videoDimension, visibilityState, pinnedAt, etc.)
|
|
8004
|
+
// as it doesn't exist on the server.
|
|
8005
|
+
const existingParticipant = participantLookup[p.sessionId];
|
|
8006
|
+
const isLocalParticipant = p.sessionId === currentSessionId;
|
|
8007
|
+
return Object.assign({}, existingParticipant, p, {
|
|
8008
|
+
isLocalParticipant,
|
|
8009
|
+
publishedTracks: isLocalParticipant
|
|
8010
|
+
? localPublishedTracks
|
|
8011
|
+
: p.publishedTracks,
|
|
8012
|
+
viewportVisibilityState: existingParticipant?.viewportVisibilityState ?? {
|
|
8013
|
+
videoTrack: exports.VisibilityState.UNKNOWN,
|
|
8014
|
+
screenShareTrack: exports.VisibilityState.UNKNOWN,
|
|
8015
|
+
},
|
|
8016
|
+
});
|
|
8017
|
+
});
|
|
8018
|
+
});
|
|
8019
|
+
this.setParticipantCount(participantCount?.total || 0);
|
|
8020
|
+
this.setAnonymousParticipantCount(participantCount?.anonymous || 0);
|
|
8021
|
+
this.setStartedAt(startedAt ? Timestamp.toDate(startedAt) : new Date());
|
|
8022
|
+
this.setServerSidePins(pins);
|
|
8023
|
+
};
|
|
7562
8024
|
this.updateFromMemberRemoved = (event) => {
|
|
7563
8025
|
this.updateFromCallResponse(event.call);
|
|
7564
8026
|
this.setCurrentValue(this.membersSubject, (members) => members.filter((m) => event.members.indexOf(m.user_id) === -1));
|
|
@@ -8270,13 +8732,79 @@ const enableHighQualityAudio = (sdp, trackMid, maxBitrate = 510000) => {
|
|
|
8270
8732
|
return SDP__namespace.write(parsedSdp);
|
|
8271
8733
|
};
|
|
8272
8734
|
|
|
8273
|
-
const
|
|
8274
|
-
|
|
8275
|
-
|
|
8276
|
-
|
|
8277
|
-
|
|
8278
|
-
|
|
8279
|
-
|
|
8735
|
+
const version = "1.6.0-0" ;
|
|
8736
|
+
const [major, minor, patch] = version.split('.');
|
|
8737
|
+
let sdkInfo = {
|
|
8738
|
+
type: SdkType.PLAIN_JAVASCRIPT,
|
|
8739
|
+
major,
|
|
8740
|
+
minor,
|
|
8741
|
+
patch,
|
|
8742
|
+
};
|
|
8743
|
+
let osInfo;
|
|
8744
|
+
let deviceInfo;
|
|
8745
|
+
let webRtcInfo;
|
|
8746
|
+
const setSdkInfo = (info) => {
|
|
8747
|
+
sdkInfo = info;
|
|
8748
|
+
};
|
|
8749
|
+
const getSdkInfo = () => {
|
|
8750
|
+
return sdkInfo;
|
|
8751
|
+
};
|
|
8752
|
+
const setOSInfo = (info) => {
|
|
8753
|
+
osInfo = info;
|
|
8754
|
+
};
|
|
8755
|
+
const getOSInfo = () => {
|
|
8756
|
+
return osInfo;
|
|
8757
|
+
};
|
|
8758
|
+
const setDeviceInfo = (info) => {
|
|
8759
|
+
deviceInfo = info;
|
|
8760
|
+
};
|
|
8761
|
+
const getDeviceInfo = () => {
|
|
8762
|
+
return deviceInfo;
|
|
8763
|
+
};
|
|
8764
|
+
const getWebRTCInfo = () => {
|
|
8765
|
+
return webRtcInfo;
|
|
8766
|
+
};
|
|
8767
|
+
const setWebRTCInfo = (info) => {
|
|
8768
|
+
webRtcInfo = info;
|
|
8769
|
+
};
|
|
8770
|
+
const getClientDetails = () => {
|
|
8771
|
+
if (isReactNative()) {
|
|
8772
|
+
// Since RN doesn't support web, sharing browser info is not required
|
|
8773
|
+
return {
|
|
8774
|
+
sdk: getSdkInfo(),
|
|
8775
|
+
os: getOSInfo(),
|
|
8776
|
+
device: getDeviceInfo(),
|
|
8777
|
+
};
|
|
8778
|
+
}
|
|
8779
|
+
const userAgent = new uaParserJs.UAParser(navigator.userAgent);
|
|
8780
|
+
const { browser, os, device, cpu } = userAgent.getResult();
|
|
8781
|
+
return {
|
|
8782
|
+
sdk: getSdkInfo(),
|
|
8783
|
+
browser: {
|
|
8784
|
+
name: browser.name || navigator.userAgent,
|
|
8785
|
+
version: browser.version || '',
|
|
8786
|
+
},
|
|
8787
|
+
os: {
|
|
8788
|
+
name: os.name || '',
|
|
8789
|
+
version: os.version || '',
|
|
8790
|
+
architecture: cpu.architecture || '',
|
|
8791
|
+
},
|
|
8792
|
+
device: {
|
|
8793
|
+
name: [device.vendor, device.model, device.type]
|
|
8794
|
+
.filter(Boolean)
|
|
8795
|
+
.join(' '),
|
|
8796
|
+
version: '',
|
|
8797
|
+
},
|
|
8798
|
+
};
|
|
8799
|
+
};
|
|
8800
|
+
|
|
8801
|
+
/**
|
|
8802
|
+
* The `Publisher` is responsible for publishing/unpublishing media streams to/from the SFU
|
|
8803
|
+
*
|
|
8804
|
+
* @internal
|
|
8805
|
+
*/
|
|
8806
|
+
class Publisher {
|
|
8807
|
+
/**
|
|
8280
8808
|
* Returns the current connection configuration.
|
|
8281
8809
|
*
|
|
8282
8810
|
* @internal
|
|
@@ -8297,8 +8825,9 @@ class Publisher {
|
|
|
8297
8825
|
* @param isRedEnabled whether RED is enabled.
|
|
8298
8826
|
* @param iceRestartDelay the delay in milliseconds to wait before restarting ICE once connection goes to `disconnected` state.
|
|
8299
8827
|
* @param onUnrecoverableError a callback to call when an unrecoverable error occurs.
|
|
8828
|
+
* @param logTag the log tag to use.
|
|
8300
8829
|
*/
|
|
8301
|
-
constructor({ connectionConfig, sfuClient, dispatcher, state, isDtxEnabled, isRedEnabled,
|
|
8830
|
+
constructor({ connectionConfig, sfuClient, dispatcher, state, isDtxEnabled, isRedEnabled, onUnrecoverableError, logTag, }) {
|
|
8302
8831
|
this.transceiverRegistry = {
|
|
8303
8832
|
[TrackType.AUDIO]: undefined,
|
|
8304
8833
|
[TrackType.VIDEO]: undefined,
|
|
@@ -8312,7 +8841,7 @@ class Publisher {
|
|
|
8312
8841
|
* This is needed because some browsers (Firefox) don't reliably report
|
|
8313
8842
|
* trackId and `mid` parameters.
|
|
8314
8843
|
*
|
|
8315
|
-
* @
|
|
8844
|
+
* @internal
|
|
8316
8845
|
*/
|
|
8317
8846
|
this.transceiverInitOrder = [];
|
|
8318
8847
|
this.trackKindMapping = {
|
|
@@ -8344,7 +8873,7 @@ class Publisher {
|
|
|
8344
8873
|
/**
|
|
8345
8874
|
* Closes the publisher PeerConnection and cleans up the resources.
|
|
8346
8875
|
*/
|
|
8347
|
-
this.close = ({ stopTracks
|
|
8876
|
+
this.close = ({ stopTracks }) => {
|
|
8348
8877
|
if (stopTracks) {
|
|
8349
8878
|
this.stopPublishing();
|
|
8350
8879
|
Object.keys(this.transceiverRegistry).forEach((trackType) => {
|
|
@@ -8356,10 +8885,22 @@ class Publisher {
|
|
|
8356
8885
|
this.trackLayersCache[trackType] = undefined;
|
|
8357
8886
|
});
|
|
8358
8887
|
}
|
|
8359
|
-
|
|
8888
|
+
this.detachEventHandlers();
|
|
8889
|
+
this.pc.close();
|
|
8890
|
+
};
|
|
8891
|
+
/**
|
|
8892
|
+
* Detaches the event handlers from the `RTCPeerConnection`.
|
|
8893
|
+
* This is useful when we want to replace the `RTCPeerConnection`
|
|
8894
|
+
* instance with a new one (in case of migration).
|
|
8895
|
+
*/
|
|
8896
|
+
this.detachEventHandlers = () => {
|
|
8360
8897
|
this.unsubscribeOnIceRestart();
|
|
8898
|
+
this.pc.removeEventListener('icecandidate', this.onIceCandidate);
|
|
8361
8899
|
this.pc.removeEventListener('negotiationneeded', this.onNegotiationNeeded);
|
|
8362
|
-
this.pc.
|
|
8900
|
+
this.pc.removeEventListener('icecandidateerror', this.onIceCandidateError);
|
|
8901
|
+
this.pc.removeEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
|
|
8902
|
+
this.pc.removeEventListener('icegatheringstatechange', this.onIceGatheringStateChange);
|
|
8903
|
+
this.pc.removeEventListener('signalingstatechange', this.onSignalingStateChange);
|
|
8363
8904
|
};
|
|
8364
8905
|
/**
|
|
8365
8906
|
* Starts publishing the given track of the given media stream.
|
|
@@ -8386,7 +8927,7 @@ class Publisher {
|
|
|
8386
8927
|
* Once the track has ended, it will notify the SFU and update the state.
|
|
8387
8928
|
*/
|
|
8388
8929
|
const handleTrackEnded = async () => {
|
|
8389
|
-
logger
|
|
8930
|
+
this.logger('info', `Track ${TrackType[trackType]} has ended, notifying the SFU`);
|
|
8390
8931
|
await this.notifyTrackMuteStateChanged(mediaStream, trackType, true);
|
|
8391
8932
|
// clean-up, this event listener needs to run only once.
|
|
8392
8933
|
track.removeEventListener('ended', handleTrackEnded);
|
|
@@ -8402,21 +8943,18 @@ class Publisher {
|
|
|
8402
8943
|
? findOptimalScreenSharingLayers(track, opts.screenShareSettings, screenShareBitrate)
|
|
8403
8944
|
: undefined;
|
|
8404
8945
|
let preferredCodec = opts.preferredCodec;
|
|
8405
|
-
if (!preferredCodec && trackType === TrackType.VIDEO) {
|
|
8406
|
-
|
|
8407
|
-
|
|
8408
|
-
if
|
|
8409
|
-
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
|
|
8413
|
-
|
|
8414
|
-
|
|
8415
|
-
preferredCodec = 'VP8';
|
|
8416
|
-
}
|
|
8946
|
+
if (!preferredCodec && trackType === TrackType.VIDEO && isReactNative()) {
|
|
8947
|
+
const osName = getOSInfo()?.name.toLowerCase();
|
|
8948
|
+
if (osName === 'ipados') {
|
|
8949
|
+
// in ipads it was noticed that if vp8 codec is used
|
|
8950
|
+
// then the bytes sent is 0 in the outbound-rtp
|
|
8951
|
+
// so we are forcing h264 codec for ipads
|
|
8952
|
+
preferredCodec = 'H264';
|
|
8953
|
+
}
|
|
8954
|
+
else if (osName === 'android') {
|
|
8955
|
+
preferredCodec = 'VP8';
|
|
8417
8956
|
}
|
|
8418
8957
|
}
|
|
8419
|
-
const codecPreferences = this.getCodecPreferences(trackType, preferredCodec);
|
|
8420
8958
|
// listen for 'ended' event on the track as it might be ended abruptly
|
|
8421
8959
|
// by an external factor as permission revokes, device disconnected, etc.
|
|
8422
8960
|
// keep in mind that `track.stop()` doesn't trigger this event.
|
|
@@ -8431,17 +8969,20 @@ class Publisher {
|
|
|
8431
8969
|
: undefined,
|
|
8432
8970
|
sendEncodings: videoEncodings,
|
|
8433
8971
|
});
|
|
8434
|
-
logger
|
|
8972
|
+
this.logger('debug', `Added ${TrackType[trackType]} transceiver`);
|
|
8435
8973
|
this.transceiverInitOrder.push(trackType);
|
|
8436
8974
|
this.transceiverRegistry[trackType] = transceiver;
|
|
8437
8975
|
this.publishOptionsPerTrackType.set(trackType, opts);
|
|
8438
|
-
|
|
8439
|
-
|
|
8976
|
+
const codecPreferences = 'setCodecPreferences' in transceiver
|
|
8977
|
+
? this.getCodecPreferences(trackType, preferredCodec)
|
|
8978
|
+
: undefined;
|
|
8979
|
+
if (codecPreferences) {
|
|
8980
|
+
this.logger('info', `Setting ${TrackType[trackType]} codec preferences`, codecPreferences);
|
|
8440
8981
|
try {
|
|
8441
8982
|
transceiver.setCodecPreferences(codecPreferences);
|
|
8442
8983
|
}
|
|
8443
8984
|
catch (err) {
|
|
8444
|
-
logger
|
|
8985
|
+
this.logger('warn', `Couldn't set codec preferences`, err);
|
|
8445
8986
|
}
|
|
8446
8987
|
}
|
|
8447
8988
|
}
|
|
@@ -8490,31 +9031,17 @@ class Publisher {
|
|
|
8490
9031
|
* @param trackType the track type to check.
|
|
8491
9032
|
*/
|
|
8492
9033
|
this.isPublishing = (trackType) => {
|
|
8493
|
-
const
|
|
8494
|
-
if (
|
|
8495
|
-
|
|
8496
|
-
|
|
8497
|
-
|
|
8498
|
-
sender.track.enabled);
|
|
8499
|
-
}
|
|
8500
|
-
return false;
|
|
8501
|
-
};
|
|
8502
|
-
/**
|
|
8503
|
-
* Returns true if the given track type is currently live
|
|
8504
|
-
*
|
|
8505
|
-
* @param trackType the track type to check.
|
|
8506
|
-
*/
|
|
8507
|
-
this.isLive = (trackType) => {
|
|
8508
|
-
const transceiverForTrackType = this.transceiverRegistry[trackType];
|
|
8509
|
-
if (transceiverForTrackType && transceiverForTrackType.sender) {
|
|
8510
|
-
const sender = transceiverForTrackType.sender;
|
|
8511
|
-
return !!sender.track && sender.track.readyState === 'live';
|
|
8512
|
-
}
|
|
8513
|
-
return false;
|
|
9034
|
+
const transceiver = this.transceiverRegistry[trackType];
|
|
9035
|
+
if (!transceiver || !transceiver.sender)
|
|
9036
|
+
return false;
|
|
9037
|
+
const track = transceiver.sender.track;
|
|
9038
|
+
return !!track && track.readyState === 'live' && track.enabled;
|
|
8514
9039
|
};
|
|
8515
9040
|
this.notifyTrackMuteStateChanged = async (mediaStream, trackType, isMuted) => {
|
|
8516
9041
|
await this.sfuClient.updateMuteState(trackType, isMuted);
|
|
8517
9042
|
const audioOrVideoOrScreenShareStream = trackTypeToParticipantStreamKey(trackType);
|
|
9043
|
+
if (!audioOrVideoOrScreenShareStream)
|
|
9044
|
+
return;
|
|
8518
9045
|
if (isMuted) {
|
|
8519
9046
|
this.state.updateParticipant(this.sfuClient.sessionId, (p) => ({
|
|
8520
9047
|
publishedTracks: p.publishedTracks.filter((t) => t !== trackType),
|
|
@@ -8536,7 +9063,7 @@ class Publisher {
|
|
|
8536
9063
|
* Stops publishing all tracks and stop all tracks.
|
|
8537
9064
|
*/
|
|
8538
9065
|
this.stopPublishing = () => {
|
|
8539
|
-
logger
|
|
9066
|
+
this.logger('debug', 'Stopping publishing all tracks');
|
|
8540
9067
|
this.pc.getSenders().forEach((s) => {
|
|
8541
9068
|
s.track?.stop();
|
|
8542
9069
|
if (this.pc.signalingState !== 'closed') {
|
|
@@ -8545,15 +9072,15 @@ class Publisher {
|
|
|
8545
9072
|
});
|
|
8546
9073
|
};
|
|
8547
9074
|
this.updateVideoPublishQuality = async (enabledLayers) => {
|
|
8548
|
-
logger
|
|
9075
|
+
this.logger('info', 'Update publish quality, requested layers by SFU:', enabledLayers);
|
|
8549
9076
|
const videoSender = this.transceiverRegistry[TrackType.VIDEO]?.sender;
|
|
8550
9077
|
if (!videoSender) {
|
|
8551
|
-
logger
|
|
9078
|
+
this.logger('warn', 'Update publish quality, no video sender found.');
|
|
8552
9079
|
return;
|
|
8553
9080
|
}
|
|
8554
9081
|
const params = videoSender.getParameters();
|
|
8555
9082
|
if (params.encodings.length === 0) {
|
|
8556
|
-
logger
|
|
9083
|
+
this.logger('warn', 'Update publish quality, No suitable video encoding quality found');
|
|
8557
9084
|
return;
|
|
8558
9085
|
}
|
|
8559
9086
|
let changed = false;
|
|
@@ -8572,18 +9099,18 @@ class Publisher {
|
|
|
8572
9099
|
if (layer !== undefined) {
|
|
8573
9100
|
if (layer.scaleResolutionDownBy >= 1 &&
|
|
8574
9101
|
layer.scaleResolutionDownBy !== enc.scaleResolutionDownBy) {
|
|
8575
|
-
logger
|
|
9102
|
+
this.logger('debug', '[dynascale]: setting scaleResolutionDownBy from server', 'layer', layer.name, 'scale-resolution-down-by', layer.scaleResolutionDownBy);
|
|
8576
9103
|
enc.scaleResolutionDownBy = layer.scaleResolutionDownBy;
|
|
8577
9104
|
changed = true;
|
|
8578
9105
|
}
|
|
8579
9106
|
if (layer.maxBitrate > 0 && layer.maxBitrate !== enc.maxBitrate) {
|
|
8580
|
-
logger
|
|
9107
|
+
this.logger('debug', '[dynascale] setting max-bitrate from the server', 'layer', layer.name, 'max-bitrate', layer.maxBitrate);
|
|
8581
9108
|
enc.maxBitrate = layer.maxBitrate;
|
|
8582
9109
|
changed = true;
|
|
8583
9110
|
}
|
|
8584
9111
|
if (layer.maxFramerate > 0 &&
|
|
8585
9112
|
layer.maxFramerate !== enc.maxFramerate) {
|
|
8586
|
-
logger
|
|
9113
|
+
this.logger('debug', '[dynascale]: setting maxFramerate from server', 'layer', layer.name, 'max-framerate', layer.maxFramerate);
|
|
8587
9114
|
enc.maxFramerate = layer.maxFramerate;
|
|
8588
9115
|
changed = true;
|
|
8589
9116
|
}
|
|
@@ -8593,10 +9120,10 @@ class Publisher {
|
|
|
8593
9120
|
const activeLayers = params.encodings.filter((e) => e.active);
|
|
8594
9121
|
if (changed) {
|
|
8595
9122
|
await videoSender.setParameters(params);
|
|
8596
|
-
logger
|
|
9123
|
+
this.logger('info', `Update publish quality, enabled rids: `, activeLayers);
|
|
8597
9124
|
}
|
|
8598
9125
|
else {
|
|
8599
|
-
logger
|
|
9126
|
+
this.logger('info', `Update publish quality, no change: `, activeLayers);
|
|
8600
9127
|
}
|
|
8601
9128
|
};
|
|
8602
9129
|
/**
|
|
@@ -8620,7 +9147,7 @@ class Publisher {
|
|
|
8620
9147
|
this.onIceCandidate = (e) => {
|
|
8621
9148
|
const { candidate } = e;
|
|
8622
9149
|
if (!candidate) {
|
|
8623
|
-
logger
|
|
9150
|
+
this.logger('debug', 'null ice candidate');
|
|
8624
9151
|
return;
|
|
8625
9152
|
}
|
|
8626
9153
|
this.sfuClient
|
|
@@ -8629,7 +9156,7 @@ class Publisher {
|
|
|
8629
9156
|
peerType: PeerType.PUBLISHER_UNSPECIFIED,
|
|
8630
9157
|
})
|
|
8631
9158
|
.catch((err) => {
|
|
8632
|
-
logger
|
|
9159
|
+
this.logger('warn', `ICETrickle failed`, err);
|
|
8633
9160
|
});
|
|
8634
9161
|
};
|
|
8635
9162
|
/**
|
|
@@ -8640,38 +9167,23 @@ class Publisher {
|
|
|
8640
9167
|
this.setSfuClient = (sfuClient) => {
|
|
8641
9168
|
this.sfuClient = sfuClient;
|
|
8642
9169
|
};
|
|
8643
|
-
/**
|
|
8644
|
-
* Performs a migration of this publisher instance to a new SFU.
|
|
8645
|
-
*
|
|
8646
|
-
* Initiates a new `iceRestart` offer/answer exchange with the new SFU.
|
|
8647
|
-
*
|
|
8648
|
-
* @param sfuClient the new SFU client to migrate to.
|
|
8649
|
-
* @param connectionConfig the new connection configuration to use.
|
|
8650
|
-
*/
|
|
8651
|
-
this.migrateTo = async (sfuClient, connectionConfig) => {
|
|
8652
|
-
this.sfuClient = sfuClient;
|
|
8653
|
-
this.pc.setConfiguration(connectionConfig);
|
|
8654
|
-
this._connectionConfiguration = connectionConfig;
|
|
8655
|
-
const shouldRestartIce = this.pc.iceConnectionState === 'connected';
|
|
8656
|
-
if (shouldRestartIce) {
|
|
8657
|
-
// negotiate only if there are tracks to publish
|
|
8658
|
-
await this.negotiate({ iceRestart: true });
|
|
8659
|
-
}
|
|
8660
|
-
};
|
|
8661
9170
|
/**
|
|
8662
9171
|
* Restarts the ICE connection and renegotiates with the SFU.
|
|
8663
9172
|
*/
|
|
8664
9173
|
this.restartIce = async () => {
|
|
8665
|
-
logger
|
|
9174
|
+
this.logger('debug', 'Restarting ICE connection');
|
|
8666
9175
|
const signalingState = this.pc.signalingState;
|
|
8667
9176
|
if (this.isIceRestarting || signalingState === 'have-local-offer') {
|
|
8668
|
-
logger
|
|
9177
|
+
this.logger('debug', 'ICE restart is already in progress');
|
|
8669
9178
|
return;
|
|
8670
9179
|
}
|
|
8671
9180
|
await this.negotiate({ iceRestart: true });
|
|
8672
9181
|
};
|
|
8673
9182
|
this.onNegotiationNeeded = () => {
|
|
8674
|
-
this.negotiate().catch((err) =>
|
|
9183
|
+
this.negotiate().catch((err) => {
|
|
9184
|
+
this.logger('warn', `Negotiation failed.`, err);
|
|
9185
|
+
this.onUnrecoverableError?.();
|
|
9186
|
+
});
|
|
8675
9187
|
};
|
|
8676
9188
|
/**
|
|
8677
9189
|
* Initiates a new offer/answer exchange with the currently connected SFU.
|
|
@@ -8683,59 +9195,62 @@ class Publisher {
|
|
|
8683
9195
|
const offer = await this.pc.createOffer(options);
|
|
8684
9196
|
let sdp = this.mungeCodecs(offer.sdp);
|
|
8685
9197
|
if (sdp && this.isPublishing(TrackType.SCREEN_SHARE_AUDIO)) {
|
|
8686
|
-
|
|
8687
|
-
if (transceiver && transceiver.sender.track) {
|
|
8688
|
-
const mid = transceiver.mid ??
|
|
8689
|
-
this.extractMid(sdp, transceiver.sender.track, TrackType.SCREEN_SHARE_AUDIO);
|
|
8690
|
-
sdp = enableHighQualityAudio(sdp, mid);
|
|
8691
|
-
}
|
|
9198
|
+
sdp = this.enableHighQualityAudio(sdp);
|
|
8692
9199
|
}
|
|
8693
9200
|
// set the munged SDP back to the offer
|
|
8694
9201
|
offer.sdp = sdp;
|
|
8695
|
-
const trackInfos = this.
|
|
9202
|
+
const trackInfos = this.getAnnouncedTracks(offer.sdp);
|
|
8696
9203
|
if (trackInfos.length === 0) {
|
|
8697
|
-
throw new Error(`Can't
|
|
9204
|
+
throw new Error(`Can't negotiate without announcing any tracks`);
|
|
8698
9205
|
}
|
|
8699
9206
|
await this.pc.setLocalDescription(offer);
|
|
8700
9207
|
const { response } = await this.sfuClient.setPublisher({
|
|
8701
9208
|
sdp: offer.sdp || '',
|
|
8702
9209
|
tracks: trackInfos,
|
|
8703
9210
|
});
|
|
9211
|
+
const { sdp: remoteSdp, error } = response;
|
|
8704
9212
|
try {
|
|
8705
|
-
await this.pc.setRemoteDescription({
|
|
8706
|
-
type: 'answer',
|
|
8707
|
-
sdp: response.sdp,
|
|
8708
|
-
});
|
|
9213
|
+
await this.pc.setRemoteDescription({ type: 'answer', sdp: remoteSdp });
|
|
8709
9214
|
}
|
|
8710
9215
|
catch (e) {
|
|
8711
|
-
logger
|
|
8712
|
-
|
|
8713
|
-
|
|
8714
|
-
|
|
9216
|
+
this.logger('error', `setRemoteDescription error`, remoteSdp, error, e);
|
|
9217
|
+
throw e;
|
|
9218
|
+
}
|
|
9219
|
+
finally {
|
|
9220
|
+
this.isIceRestarting = false;
|
|
8715
9221
|
}
|
|
8716
|
-
this.isIceRestarting = false;
|
|
8717
9222
|
this.sfuClient.iceTrickleBuffer.publisherCandidates.subscribe(async (candidate) => {
|
|
8718
9223
|
try {
|
|
8719
9224
|
const iceCandidate = JSON.parse(candidate.iceCandidate);
|
|
8720
9225
|
await this.pc.addIceCandidate(iceCandidate);
|
|
8721
9226
|
}
|
|
8722
9227
|
catch (e) {
|
|
8723
|
-
logger
|
|
9228
|
+
this.logger('warn', `ICE candidate error`, e, candidate);
|
|
8724
9229
|
}
|
|
8725
9230
|
});
|
|
8726
9231
|
};
|
|
9232
|
+
this.enableHighQualityAudio = (sdp) => {
|
|
9233
|
+
const transceiver = this.transceiverRegistry[TrackType.SCREEN_SHARE_AUDIO];
|
|
9234
|
+
if (!transceiver)
|
|
9235
|
+
return sdp;
|
|
9236
|
+
const mid = this.extractMid(transceiver, sdp, TrackType.SCREEN_SHARE_AUDIO);
|
|
9237
|
+
return enableHighQualityAudio(sdp, mid);
|
|
9238
|
+
};
|
|
8727
9239
|
this.mungeCodecs = (sdp) => {
|
|
8728
9240
|
if (sdp) {
|
|
8729
9241
|
sdp = toggleDtx(sdp, this.isDtxEnabled);
|
|
8730
9242
|
}
|
|
8731
9243
|
return sdp;
|
|
8732
9244
|
};
|
|
8733
|
-
this.extractMid = (
|
|
9245
|
+
this.extractMid = (transceiver, sdp, trackType) => {
|
|
9246
|
+
if (transceiver.mid)
|
|
9247
|
+
return transceiver.mid;
|
|
8734
9248
|
if (!sdp) {
|
|
8735
|
-
logger
|
|
9249
|
+
this.logger('warn', 'No SDP found. Returning empty mid');
|
|
8736
9250
|
return '';
|
|
8737
9251
|
}
|
|
8738
|
-
logger
|
|
9252
|
+
this.logger('debug', `No 'mid' found for track. Trying to find it from the Offer SDP`);
|
|
9253
|
+
const track = transceiver.sender.track;
|
|
8739
9254
|
const parsedSdp = SDP__namespace.parse(sdp);
|
|
8740
9255
|
const media = parsedSdp.media.find((m) => {
|
|
8741
9256
|
return (m.type === track.kind &&
|
|
@@ -8743,17 +9258,23 @@ class Publisher {
|
|
|
8743
9258
|
(m.msid?.includes(track.id) ?? true));
|
|
8744
9259
|
});
|
|
8745
9260
|
if (typeof media?.mid === 'undefined') {
|
|
8746
|
-
logger
|
|
9261
|
+
this.logger('debug', `No mid found in SDP for track type ${track.kind} and id ${track.id}. Attempting to find it heuristically`);
|
|
8747
9262
|
const heuristicMid = this.transceiverInitOrder.indexOf(trackType);
|
|
8748
9263
|
if (heuristicMid !== -1) {
|
|
8749
9264
|
return String(heuristicMid);
|
|
8750
9265
|
}
|
|
8751
|
-
logger
|
|
9266
|
+
this.logger('debug', 'No heuristic mid found. Returning empty mid');
|
|
8752
9267
|
return '';
|
|
8753
9268
|
}
|
|
8754
9269
|
return String(media.mid);
|
|
8755
9270
|
};
|
|
8756
|
-
|
|
9271
|
+
/**
|
|
9272
|
+
* Returns a list of tracks that are currently being published.
|
|
9273
|
+
*
|
|
9274
|
+
* @internal
|
|
9275
|
+
* @param sdp an optional SDP to extract the `mid` from.
|
|
9276
|
+
*/
|
|
9277
|
+
this.getAnnouncedTracks = (sdp) => {
|
|
8757
9278
|
sdp = sdp || this.pc.localDescription?.sdp;
|
|
8758
9279
|
const { settings } = this.state;
|
|
8759
9280
|
const targetResolution = settings?.video
|
|
@@ -8765,7 +9286,8 @@ class Publisher {
|
|
|
8765
9286
|
const trackType = Number(Object.keys(this.transceiverRegistry).find((key) => this.transceiverRegistry[key] === transceiver));
|
|
8766
9287
|
const track = transceiver.sender.track;
|
|
8767
9288
|
let optimalLayers;
|
|
8768
|
-
|
|
9289
|
+
const isTrackLive = track.readyState === 'live';
|
|
9290
|
+
if (isTrackLive) {
|
|
8769
9291
|
const publishOpts = this.publishOptionsPerTrackType.get(trackType);
|
|
8770
9292
|
optimalLayers =
|
|
8771
9293
|
trackType === TrackType.VIDEO
|
|
@@ -8778,7 +9300,7 @@ class Publisher {
|
|
|
8778
9300
|
else {
|
|
8779
9301
|
// we report the last known optimal layers for ended tracks
|
|
8780
9302
|
optimalLayers = this.trackLayersCache[trackType] || [];
|
|
8781
|
-
logger
|
|
9303
|
+
this.logger('debug', `Track ${TrackType[trackType]} is ended. Announcing last known optimal layers`, optimalLayers);
|
|
8782
9304
|
}
|
|
8783
9305
|
const layers = optimalLayers.map((optimalLayer) => ({
|
|
8784
9306
|
rid: optimalLayer.rid || '',
|
|
@@ -8800,10 +9322,11 @@ class Publisher {
|
|
|
8800
9322
|
trackId: track.id,
|
|
8801
9323
|
layers: layers,
|
|
8802
9324
|
trackType,
|
|
8803
|
-
mid:
|
|
9325
|
+
mid: this.extractMid(transceiver, sdp, trackType),
|
|
8804
9326
|
stereo: isStereo,
|
|
8805
9327
|
dtx: isAudioTrack && this.isDtxEnabled,
|
|
8806
9328
|
red: isAudioTrack && this.isRedEnabled,
|
|
9329
|
+
muted: !isTrackLive,
|
|
8807
9330
|
};
|
|
8808
9331
|
});
|
|
8809
9332
|
};
|
|
@@ -8812,44 +9335,26 @@ class Publisher {
|
|
|
8812
9335
|
`${e.errorCode}: ${e.errorText}`;
|
|
8813
9336
|
const iceState = this.pc.iceConnectionState;
|
|
8814
9337
|
const logLevel = iceState === 'connected' || iceState === 'checking' ? 'debug' : 'warn';
|
|
8815
|
-
logger
|
|
9338
|
+
this.logger(logLevel, `ICE Candidate error`, errorMessage);
|
|
8816
9339
|
};
|
|
8817
9340
|
this.onIceConnectionStateChange = () => {
|
|
8818
9341
|
const state = this.pc.iceConnectionState;
|
|
8819
|
-
logger
|
|
8820
|
-
|
|
8821
|
-
|
|
8822
|
-
|
|
9342
|
+
this.logger('debug', `ICE Connection state changed to`, state);
|
|
9343
|
+
if (this.state.callingState === exports.CallingState.RECONNECTING)
|
|
9344
|
+
return;
|
|
9345
|
+
if (state === 'failed' || state === 'disconnected') {
|
|
9346
|
+
this.logger('debug', `Attempting to restart ICE`);
|
|
8823
9347
|
this.restartIce().catch((e) => {
|
|
8824
|
-
logger
|
|
9348
|
+
this.logger('error', `ICE restart error`, e);
|
|
8825
9349
|
this.onUnrecoverableError?.();
|
|
8826
9350
|
});
|
|
8827
9351
|
}
|
|
8828
|
-
else if (state === 'disconnected' && hasNetworkConnection) {
|
|
8829
|
-
// when in `disconnected` state, the browser may recover automatically,
|
|
8830
|
-
// hence, we delay the ICE restart
|
|
8831
|
-
logger$3('debug', `Scheduling ICE restart in ${this.iceRestartDelay} ms.`);
|
|
8832
|
-
this.iceRestartTimeout = setTimeout(() => {
|
|
8833
|
-
// check if the state is still `disconnected` or `failed`
|
|
8834
|
-
// as the connection may have recovered (or failed) in the meantime
|
|
8835
|
-
if (this.pc.iceConnectionState === 'disconnected' ||
|
|
8836
|
-
this.pc.iceConnectionState === 'failed') {
|
|
8837
|
-
this.restartIce().catch((e) => {
|
|
8838
|
-
logger$3('error', `ICE restart error`, e);
|
|
8839
|
-
this.onUnrecoverableError?.();
|
|
8840
|
-
});
|
|
8841
|
-
}
|
|
8842
|
-
else {
|
|
8843
|
-
logger$3('debug', `Scheduled ICE restart: connection recovered, canceled.`);
|
|
8844
|
-
}
|
|
8845
|
-
}, this.iceRestartDelay);
|
|
8846
|
-
}
|
|
8847
9352
|
};
|
|
8848
9353
|
this.onIceGatheringStateChange = () => {
|
|
8849
|
-
logger
|
|
9354
|
+
this.logger('debug', `ICE Gathering State`, this.pc.iceGatheringState);
|
|
8850
9355
|
};
|
|
8851
9356
|
this.onSignalingStateChange = () => {
|
|
8852
|
-
logger
|
|
9357
|
+
this.logger('debug', `Signaling state changed`, this.pc.signalingState);
|
|
8853
9358
|
};
|
|
8854
9359
|
this.ridToVideoQuality = (rid) => {
|
|
8855
9360
|
return rid === 'q'
|
|
@@ -8858,28 +9363,29 @@ class Publisher {
|
|
|
8858
9363
|
? VideoQuality.MID
|
|
8859
9364
|
: VideoQuality.HIGH; // default to HIGH
|
|
8860
9365
|
};
|
|
9366
|
+
this.logger = getLogger(['Publisher', logTag]);
|
|
8861
9367
|
this.pc = this.createPeerConnection(connectionConfig);
|
|
8862
9368
|
this.sfuClient = sfuClient;
|
|
8863
9369
|
this.state = state;
|
|
8864
9370
|
this.isDtxEnabled = isDtxEnabled;
|
|
8865
9371
|
this.isRedEnabled = isRedEnabled;
|
|
8866
|
-
this.iceRestartDelay = iceRestartDelay;
|
|
8867
9372
|
this.onUnrecoverableError = onUnrecoverableError;
|
|
8868
9373
|
this.unsubscribeOnIceRestart = dispatcher.on('iceRestart', (iceRestart) => {
|
|
8869
9374
|
if (iceRestart.peerType !== PeerType.PUBLISHER_UNSPECIFIED)
|
|
8870
9375
|
return;
|
|
8871
9376
|
this.restartIce().catch((err) => {
|
|
8872
|
-
logger
|
|
9377
|
+
this.logger('warn', `ICERestart failed`, err);
|
|
8873
9378
|
this.onUnrecoverableError?.();
|
|
8874
9379
|
});
|
|
8875
9380
|
});
|
|
8876
9381
|
}
|
|
8877
9382
|
}
|
|
8878
9383
|
|
|
8879
|
-
const logger$2 = getLogger(['Subscriber']);
|
|
8880
9384
|
/**
|
|
8881
9385
|
* A wrapper around the `RTCPeerConnection` that handles the incoming
|
|
8882
9386
|
* media streams from the SFU.
|
|
9387
|
+
*
|
|
9388
|
+
* @internal
|
|
8883
9389
|
*/
|
|
8884
9390
|
class Subscriber {
|
|
8885
9391
|
/**
|
|
@@ -8901,8 +9407,9 @@ class Subscriber {
|
|
|
8901
9407
|
* @param connectionConfig the connection configuration to use.
|
|
8902
9408
|
* @param iceRestartDelay the delay in milliseconds to wait before restarting ICE when connection goes to `disconnected` state.
|
|
8903
9409
|
* @param onUnrecoverableError a callback to call when an unrecoverable error occurs.
|
|
9410
|
+
* @param logTag a tag to use for logging.
|
|
8904
9411
|
*/
|
|
8905
|
-
constructor({ sfuClient, dispatcher, state, connectionConfig,
|
|
9412
|
+
constructor({ sfuClient, dispatcher, state, connectionConfig, onUnrecoverableError, logTag, }) {
|
|
8906
9413
|
this.isIceRestarting = false;
|
|
8907
9414
|
/**
|
|
8908
9415
|
* Creates a new `RTCPeerConnection` instance with the given configuration.
|
|
@@ -8923,10 +9430,22 @@ class Subscriber {
|
|
|
8923
9430
|
* Closes the `RTCPeerConnection` and unsubscribes from the dispatcher.
|
|
8924
9431
|
*/
|
|
8925
9432
|
this.close = () => {
|
|
8926
|
-
|
|
9433
|
+
this.detachEventHandlers();
|
|
9434
|
+
this.pc.close();
|
|
9435
|
+
};
|
|
9436
|
+
/**
|
|
9437
|
+
* Detaches the event handlers from the `RTCPeerConnection`.
|
|
9438
|
+
* This is useful when we want to replace the `RTCPeerConnection`
|
|
9439
|
+
* instance with a new one (in case of migration).
|
|
9440
|
+
*/
|
|
9441
|
+
this.detachEventHandlers = () => {
|
|
8927
9442
|
this.unregisterOnSubscriberOffer();
|
|
8928
9443
|
this.unregisterOnIceRestart();
|
|
8929
|
-
this.pc.
|
|
9444
|
+
this.pc.removeEventListener('icecandidate', this.onIceCandidate);
|
|
9445
|
+
this.pc.removeEventListener('track', this.handleOnTrack);
|
|
9446
|
+
this.pc.removeEventListener('icecandidateerror', this.onIceCandidateError);
|
|
9447
|
+
this.pc.removeEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
|
|
9448
|
+
this.pc.removeEventListener('icegatheringstatechange', this.onIceGatheringStateChange);
|
|
8930
9449
|
};
|
|
8931
9450
|
/**
|
|
8932
9451
|
* Returns the result of the `RTCPeerConnection.getStats()` method
|
|
@@ -8944,76 +9463,17 @@ class Subscriber {
|
|
|
8944
9463
|
this.setSfuClient = (sfuClient) => {
|
|
8945
9464
|
this.sfuClient = sfuClient;
|
|
8946
9465
|
};
|
|
8947
|
-
/**
|
|
8948
|
-
* Migrates the subscriber to a new SFU client.
|
|
8949
|
-
*
|
|
8950
|
-
* @param sfuClient the new SFU client to migrate to.
|
|
8951
|
-
* @param connectionConfig the new connection configuration to use.
|
|
8952
|
-
*/
|
|
8953
|
-
this.migrateTo = (sfuClient, connectionConfig) => {
|
|
8954
|
-
this.setSfuClient(sfuClient);
|
|
8955
|
-
// when migrating, we want to keep the previous subscriber open
|
|
8956
|
-
// until the new one is connected
|
|
8957
|
-
const previousPC = this.pc;
|
|
8958
|
-
// we keep a record of previously available video tracks
|
|
8959
|
-
// so that we can monitor when they become available on the new
|
|
8960
|
-
// subscriber and close the previous one.
|
|
8961
|
-
const trackIdsToMigrate = new Set();
|
|
8962
|
-
previousPC.getReceivers().forEach((r) => {
|
|
8963
|
-
if (r.track.kind === 'video') {
|
|
8964
|
-
trackIdsToMigrate.add(r.track.id);
|
|
8965
|
-
}
|
|
8966
|
-
});
|
|
8967
|
-
// set up a new subscriber peer connection, configured to connect
|
|
8968
|
-
// to the new SFU node
|
|
8969
|
-
const pc = this.createPeerConnection(connectionConfig);
|
|
8970
|
-
let migrationTimeoutId;
|
|
8971
|
-
const cleanupMigration = () => {
|
|
8972
|
-
previousPC.close();
|
|
8973
|
-
clearTimeout(migrationTimeoutId);
|
|
8974
|
-
};
|
|
8975
|
-
// When migrating, we want to keep track of the video tracks
|
|
8976
|
-
// that are migrating to the new subscriber.
|
|
8977
|
-
// Once all of them are available, we can close the previous subscriber.
|
|
8978
|
-
const handleTrackMigration = (e) => {
|
|
8979
|
-
logger$2('debug', `[Migration]: Migrated track: ${e.track.id}, ${e.track.kind}`);
|
|
8980
|
-
trackIdsToMigrate.delete(e.track.id);
|
|
8981
|
-
if (trackIdsToMigrate.size === 0) {
|
|
8982
|
-
logger$2('debug', `[Migration]: Migration complete`);
|
|
8983
|
-
pc.removeEventListener('track', handleTrackMigration);
|
|
8984
|
-
cleanupMigration();
|
|
8985
|
-
}
|
|
8986
|
-
};
|
|
8987
|
-
// When migrating, we want to keep track of the connection state
|
|
8988
|
-
// of the new subscriber.
|
|
8989
|
-
// Once it is connected, we give it a 2-second grace period to receive
|
|
8990
|
-
// all the video tracks that are migrating from the previous subscriber.
|
|
8991
|
-
// After this threshold, we abruptly close the previous subscriber.
|
|
8992
|
-
const handleConnectionStateChange = () => {
|
|
8993
|
-
if (pc.connectionState === 'connected') {
|
|
8994
|
-
migrationTimeoutId = setTimeout(() => {
|
|
8995
|
-
pc.removeEventListener('track', handleTrackMigration);
|
|
8996
|
-
cleanupMigration();
|
|
8997
|
-
}, 2000);
|
|
8998
|
-
pc.removeEventListener('connectionstatechange', handleConnectionStateChange);
|
|
8999
|
-
}
|
|
9000
|
-
};
|
|
9001
|
-
pc.addEventListener('track', handleTrackMigration);
|
|
9002
|
-
pc.addEventListener('connectionstatechange', handleConnectionStateChange);
|
|
9003
|
-
// replace the PeerConnection instance
|
|
9004
|
-
this.pc = pc;
|
|
9005
|
-
};
|
|
9006
9466
|
/**
|
|
9007
9467
|
* Restarts the ICE connection and renegotiates with the SFU.
|
|
9008
9468
|
*/
|
|
9009
9469
|
this.restartIce = async () => {
|
|
9010
|
-
logger
|
|
9470
|
+
this.logger('debug', 'Restarting ICE connection');
|
|
9011
9471
|
if (this.pc.signalingState === 'have-remote-offer') {
|
|
9012
|
-
logger
|
|
9472
|
+
this.logger('debug', 'ICE restart is already in progress');
|
|
9013
9473
|
return;
|
|
9014
9474
|
}
|
|
9015
9475
|
if (this.pc.connectionState === 'new') {
|
|
9016
|
-
logger
|
|
9476
|
+
this.logger('debug', `ICE connection is not yet established, skipping restart.`);
|
|
9017
9477
|
return;
|
|
9018
9478
|
}
|
|
9019
9479
|
const previousIsIceRestarting = this.isIceRestarting;
|
|
@@ -9032,36 +9492,42 @@ class Subscriber {
|
|
|
9032
9492
|
this.handleOnTrack = (e) => {
|
|
9033
9493
|
const [primaryStream] = e.streams;
|
|
9034
9494
|
// example: `e3f6aaf8-b03d-4911-be36-83f47d37a76a:TRACK_TYPE_VIDEO`
|
|
9035
|
-
const [trackId,
|
|
9495
|
+
const [trackId, rawTrackType] = primaryStream.id.split(':');
|
|
9036
9496
|
const participantToUpdate = this.state.participants.find((p) => p.trackLookupPrefix === trackId);
|
|
9037
|
-
logger
|
|
9038
|
-
|
|
9039
|
-
logger$2('warn', `[onTrack]: Received track for unknown participant: ${trackId}`, e);
|
|
9040
|
-
return;
|
|
9041
|
-
}
|
|
9042
|
-
const trackDebugInfo = `${participantToUpdate.userId} ${trackType}:${trackId}`;
|
|
9497
|
+
this.logger('debug', `[onTrack]: Got remote ${rawTrackType} track for userId: ${participantToUpdate?.userId}`, e.track.id, e.track);
|
|
9498
|
+
const trackDebugInfo = `${participantToUpdate?.userId} ${rawTrackType}:${trackId}`;
|
|
9043
9499
|
e.track.addEventListener('mute', () => {
|
|
9044
|
-
logger
|
|
9500
|
+
this.logger('info', `[onTrack]: Track muted: ${trackDebugInfo}`);
|
|
9045
9501
|
});
|
|
9046
9502
|
e.track.addEventListener('unmute', () => {
|
|
9047
|
-
logger
|
|
9503
|
+
this.logger('info', `[onTrack]: Track unmuted: ${trackDebugInfo}`);
|
|
9048
9504
|
});
|
|
9049
9505
|
e.track.addEventListener('ended', () => {
|
|
9050
|
-
logger
|
|
9506
|
+
this.logger('info', `[onTrack]: Track ended: ${trackDebugInfo}`);
|
|
9507
|
+
this.state.removeOrphanedTrack(primaryStream.id);
|
|
9051
9508
|
});
|
|
9052
|
-
const
|
|
9053
|
-
|
|
9054
|
-
|
|
9055
|
-
|
|
9056
|
-
|
|
9057
|
-
|
|
9509
|
+
const trackType = toTrackType(rawTrackType);
|
|
9510
|
+
if (!trackType) {
|
|
9511
|
+
return this.logger('error', `Unknown track type: ${rawTrackType}`);
|
|
9512
|
+
}
|
|
9513
|
+
if (!participantToUpdate) {
|
|
9514
|
+
this.logger('warn', `[onTrack]: Received track for unknown participant: ${trackId}`, e);
|
|
9515
|
+
this.state.registerOrphanedTrack({
|
|
9516
|
+
id: primaryStream.id,
|
|
9517
|
+
trackLookupPrefix: trackId,
|
|
9518
|
+
track: primaryStream,
|
|
9519
|
+
trackType,
|
|
9520
|
+
});
|
|
9521
|
+
return;
|
|
9522
|
+
}
|
|
9523
|
+
const streamKindProp = trackTypeToParticipantStreamKey(trackType);
|
|
9058
9524
|
if (!streamKindProp) {
|
|
9059
|
-
logger
|
|
9525
|
+
this.logger('error', `Unknown track type: ${rawTrackType}`);
|
|
9060
9526
|
return;
|
|
9061
9527
|
}
|
|
9062
9528
|
const previousStream = participantToUpdate[streamKindProp];
|
|
9063
9529
|
if (previousStream) {
|
|
9064
|
-
logger
|
|
9530
|
+
this.logger('info', `[onTrack]: Cleaning up previous remote ${e.track.kind} tracks for userId: ${participantToUpdate.userId}`);
|
|
9065
9531
|
previousStream.getTracks().forEach((t) => {
|
|
9066
9532
|
t.stop();
|
|
9067
9533
|
previousStream.removeTrack(t);
|
|
@@ -9074,7 +9540,7 @@ class Subscriber {
|
|
|
9074
9540
|
this.onIceCandidate = (e) => {
|
|
9075
9541
|
const { candidate } = e;
|
|
9076
9542
|
if (!candidate) {
|
|
9077
|
-
logger
|
|
9543
|
+
this.logger('debug', 'null ice candidate');
|
|
9078
9544
|
return;
|
|
9079
9545
|
}
|
|
9080
9546
|
this.sfuClient
|
|
@@ -9083,11 +9549,11 @@ class Subscriber {
|
|
|
9083
9549
|
peerType: PeerType.SUBSCRIBER,
|
|
9084
9550
|
})
|
|
9085
9551
|
.catch((err) => {
|
|
9086
|
-
logger
|
|
9552
|
+
this.logger('warn', `ICETrickle failed`, err);
|
|
9087
9553
|
});
|
|
9088
9554
|
};
|
|
9089
9555
|
this.negotiate = async (subscriberOffer) => {
|
|
9090
|
-
logger
|
|
9556
|
+
this.logger('info', `Received subscriberOffer`, subscriberOffer);
|
|
9091
9557
|
await this.pc.setRemoteDescription({
|
|
9092
9558
|
type: 'offer',
|
|
9093
9559
|
sdp: subscriberOffer.sdp,
|
|
@@ -9098,7 +9564,7 @@ class Subscriber {
|
|
|
9098
9564
|
await this.pc.addIceCandidate(iceCandidate);
|
|
9099
9565
|
}
|
|
9100
9566
|
catch (e) {
|
|
9101
|
-
logger
|
|
9567
|
+
this.logger('warn', `ICE candidate error`, [e, candidate]);
|
|
9102
9568
|
}
|
|
9103
9569
|
});
|
|
9104
9570
|
const answer = await this.pc.createAnswer();
|
|
@@ -9111,63 +9577,51 @@ class Subscriber {
|
|
|
9111
9577
|
};
|
|
9112
9578
|
this.onIceConnectionStateChange = () => {
|
|
9113
9579
|
const state = this.pc.iceConnectionState;
|
|
9114
|
-
logger
|
|
9580
|
+
this.logger('debug', `ICE connection state changed`, state);
|
|
9581
|
+
if (this.state.callingState === exports.CallingState.RECONNECTING)
|
|
9582
|
+
return;
|
|
9115
9583
|
// do nothing when ICE is restarting
|
|
9116
9584
|
if (this.isIceRestarting)
|
|
9117
9585
|
return;
|
|
9118
|
-
|
|
9119
|
-
|
|
9120
|
-
logger$2('debug', `Attempting to restart ICE`);
|
|
9586
|
+
if (state === 'failed' || state === 'disconnected') {
|
|
9587
|
+
this.logger('debug', `Attempting to restart ICE`);
|
|
9121
9588
|
this.restartIce().catch((e) => {
|
|
9122
|
-
logger
|
|
9589
|
+
this.logger('error', `ICE restart failed`, e);
|
|
9123
9590
|
this.onUnrecoverableError?.();
|
|
9124
9591
|
});
|
|
9125
9592
|
}
|
|
9126
|
-
else if (state === 'disconnected' && hasNetworkConnection) {
|
|
9127
|
-
// when in `disconnected` state, the browser may recover automatically,
|
|
9128
|
-
// hence, we delay the ICE restart
|
|
9129
|
-
logger$2('debug', `Scheduling ICE restart in ${this.iceRestartDelay} ms.`);
|
|
9130
|
-
this.iceRestartTimeout = setTimeout(() => {
|
|
9131
|
-
// check if the state is still `disconnected` or `failed`
|
|
9132
|
-
// as the connection may have recovered (or failed) in the meantime
|
|
9133
|
-
if (this.pc.iceConnectionState === 'disconnected' ||
|
|
9134
|
-
this.pc.iceConnectionState === 'failed') {
|
|
9135
|
-
this.restartIce().catch((e) => {
|
|
9136
|
-
logger$2('error', `ICE restart failed`, e);
|
|
9137
|
-
this.onUnrecoverableError?.();
|
|
9138
|
-
});
|
|
9139
|
-
}
|
|
9140
|
-
else {
|
|
9141
|
-
logger$2('debug', `Scheduled ICE restart: connection recovered, canceled.`);
|
|
9142
|
-
}
|
|
9143
|
-
}, this.iceRestartDelay);
|
|
9144
|
-
}
|
|
9145
9593
|
};
|
|
9146
9594
|
this.onIceGatheringStateChange = () => {
|
|
9147
|
-
logger
|
|
9595
|
+
this.logger('debug', `ICE gathering state changed`, this.pc.iceGatheringState);
|
|
9148
9596
|
};
|
|
9149
9597
|
this.onIceCandidateError = (e) => {
|
|
9150
9598
|
const errorMessage = e instanceof RTCPeerConnectionIceErrorEvent &&
|
|
9151
9599
|
`${e.errorCode}: ${e.errorText}`;
|
|
9152
9600
|
const iceState = this.pc.iceConnectionState;
|
|
9153
9601
|
const logLevel = iceState === 'connected' || iceState === 'checking' ? 'debug' : 'warn';
|
|
9154
|
-
logger
|
|
9602
|
+
this.logger(logLevel, `ICE Candidate error`, errorMessage);
|
|
9155
9603
|
};
|
|
9604
|
+
this.logger = getLogger(['Subscriber', logTag]);
|
|
9156
9605
|
this.sfuClient = sfuClient;
|
|
9157
9606
|
this.state = state;
|
|
9158
|
-
this.iceRestartDelay = iceRestartDelay;
|
|
9159
9607
|
this.onUnrecoverableError = onUnrecoverableError;
|
|
9160
9608
|
this.pc = this.createPeerConnection(connectionConfig);
|
|
9609
|
+
const subscriberOfferConcurrencyTag = Symbol('subscriberOffer');
|
|
9161
9610
|
this.unregisterOnSubscriberOffer = dispatcher.on('subscriberOffer', (subscriberOffer) => {
|
|
9162
|
-
|
|
9163
|
-
|
|
9611
|
+
withoutConcurrency(subscriberOfferConcurrencyTag, () => {
|
|
9612
|
+
return this.negotiate(subscriberOffer);
|
|
9613
|
+
}).catch((err) => {
|
|
9614
|
+
this.logger('warn', `Negotiation failed.`, err);
|
|
9164
9615
|
});
|
|
9165
9616
|
});
|
|
9617
|
+
const iceRestartConcurrencyTag = Symbol('iceRestart');
|
|
9166
9618
|
this.unregisterOnIceRestart = dispatcher.on('iceRestart', (iceRestart) => {
|
|
9167
|
-
|
|
9168
|
-
|
|
9169
|
-
|
|
9170
|
-
|
|
9619
|
+
withoutConcurrency(iceRestartConcurrencyTag, async () => {
|
|
9620
|
+
if (iceRestart.peerType !== PeerType.SUBSCRIBER)
|
|
9621
|
+
return;
|
|
9622
|
+
await this.restartIce();
|
|
9623
|
+
}).catch((err) => {
|
|
9624
|
+
this.logger('warn', `ICERestart failed`, err);
|
|
9171
9625
|
this.onUnrecoverableError?.();
|
|
9172
9626
|
});
|
|
9173
9627
|
});
|
|
@@ -9175,8 +9629,8 @@ class Subscriber {
|
|
|
9175
9629
|
}
|
|
9176
9630
|
|
|
9177
9631
|
const createWebSocketSignalChannel = (opts) => {
|
|
9178
|
-
const
|
|
9179
|
-
const
|
|
9632
|
+
const { endpoint, onMessage, logTag } = opts;
|
|
9633
|
+
const logger = getLogger(['sfu-client-ws', logTag]);
|
|
9180
9634
|
const ws = new WebSocket(endpoint);
|
|
9181
9635
|
ws.binaryType = 'arraybuffer'; // do we need this?
|
|
9182
9636
|
ws.addEventListener('error', (e) => {
|
|
@@ -9194,140 +9648,45 @@ const createWebSocketSignalChannel = (opts) => {
|
|
|
9194
9648
|
? SfuEvent.fromBinary(new Uint8Array(e.data))
|
|
9195
9649
|
: SfuEvent.fromJsonString(e.data.toString());
|
|
9196
9650
|
onMessage(message);
|
|
9197
|
-
}
|
|
9198
|
-
catch (err) {
|
|
9199
|
-
logger('error', 'Failed to decode a message. Check whether the Proto models match.', { event: e, error: err });
|
|
9200
|
-
}
|
|
9201
|
-
});
|
|
9202
|
-
return ws;
|
|
9203
|
-
};
|
|
9204
|
-
|
|
9205
|
-
|
|
9206
|
-
|
|
9207
|
-
|
|
9208
|
-
|
|
9209
|
-
|
|
9210
|
-
|
|
9211
|
-
|
|
9212
|
-
|
|
9213
|
-
|
|
9214
|
-
|
|
9215
|
-
|
|
9216
|
-
|
|
9217
|
-
|
|
9218
|
-
|
|
9219
|
-
|
|
9220
|
-
|
|
9221
|
-
|
|
9222
|
-
|
|
9223
|
-
|
|
9224
|
-
|
|
9225
|
-
|
|
9226
|
-
|
|
9227
|
-
|
|
9228
|
-
|
|
9229
|
-
|
|
9230
|
-
|
|
9231
|
-
|
|
9232
|
-
|
|
9233
|
-
|
|
9234
|
-
}
|
|
9235
|
-
|
|
9236
|
-
let s = '';
|
|
9237
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
9238
|
-
s += bytes[i].toString(16).padStart(2, '0');
|
|
9239
|
-
}
|
|
9240
|
-
return s;
|
|
9241
|
-
}
|
|
9242
|
-
// https://tools.ietf.org/html/rfc4122
|
|
9243
|
-
function generateUUIDv4() {
|
|
9244
|
-
const bytes = getRandomBytes(16);
|
|
9245
|
-
bytes[6] = (bytes[6] & 0x0f) | 0x40; // version
|
|
9246
|
-
bytes[8] = (bytes[8] & 0xbf) | 0x80; // variant
|
|
9247
|
-
return (hex(bytes.subarray(0, 4)) +
|
|
9248
|
-
'-' +
|
|
9249
|
-
hex(bytes.subarray(4, 6)) +
|
|
9250
|
-
'-' +
|
|
9251
|
-
hex(bytes.subarray(6, 8)) +
|
|
9252
|
-
'-' +
|
|
9253
|
-
hex(bytes.subarray(8, 10)) +
|
|
9254
|
-
'-' +
|
|
9255
|
-
hex(bytes.subarray(10, 16)));
|
|
9256
|
-
}
|
|
9257
|
-
function getRandomValuesWithMathRandom(bytes) {
|
|
9258
|
-
const max = Math.pow(2, (8 * bytes.byteLength) / bytes.length);
|
|
9259
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
9260
|
-
bytes[i] = Math.random() * max;
|
|
9261
|
-
}
|
|
9262
|
-
}
|
|
9263
|
-
const getRandomValues = (() => {
|
|
9264
|
-
if (typeof crypto !== 'undefined' &&
|
|
9265
|
-
typeof crypto?.getRandomValues !== 'undefined') {
|
|
9266
|
-
return crypto.getRandomValues.bind(crypto);
|
|
9267
|
-
}
|
|
9268
|
-
else if (typeof msCrypto !== 'undefined') {
|
|
9269
|
-
return msCrypto.getRandomValues.bind(msCrypto);
|
|
9270
|
-
}
|
|
9271
|
-
else {
|
|
9272
|
-
return getRandomValuesWithMathRandom;
|
|
9273
|
-
}
|
|
9274
|
-
})();
|
|
9275
|
-
function getRandomBytes(length) {
|
|
9276
|
-
const bytes = new Uint8Array(length);
|
|
9277
|
-
getRandomValues(bytes);
|
|
9278
|
-
return bytes;
|
|
9279
|
-
}
|
|
9280
|
-
function convertErrorToJson(err) {
|
|
9281
|
-
const jsonObj = {};
|
|
9282
|
-
if (!err)
|
|
9283
|
-
return jsonObj;
|
|
9284
|
-
try {
|
|
9285
|
-
Object.getOwnPropertyNames(err).forEach((key) => {
|
|
9286
|
-
jsonObj[key] = Object.getOwnPropertyDescriptor(err, key);
|
|
9287
|
-
});
|
|
9288
|
-
}
|
|
9289
|
-
catch (_) {
|
|
9290
|
-
return {
|
|
9291
|
-
error: 'failed to serialize the error',
|
|
9292
|
-
};
|
|
9293
|
-
}
|
|
9294
|
-
return jsonObj;
|
|
9295
|
-
}
|
|
9296
|
-
/**
|
|
9297
|
-
* isOnline safely return the navigator.online value for browser env
|
|
9298
|
-
* if navigator is not in global object, it always return true
|
|
9299
|
-
*/
|
|
9300
|
-
function isOnline(logger) {
|
|
9301
|
-
const nav = typeof navigator !== 'undefined'
|
|
9302
|
-
? navigator
|
|
9303
|
-
: typeof window !== 'undefined' && window.navigator
|
|
9304
|
-
? window.navigator
|
|
9305
|
-
: undefined;
|
|
9306
|
-
if (!nav) {
|
|
9307
|
-
logger('warn', 'isOnline failed to access window.navigator and assume browser is online');
|
|
9308
|
-
return true;
|
|
9309
|
-
}
|
|
9310
|
-
// RN navigator has undefined for onLine
|
|
9311
|
-
if (typeof nav.onLine !== 'boolean') {
|
|
9312
|
-
return true;
|
|
9313
|
-
}
|
|
9314
|
-
return nav.onLine;
|
|
9315
|
-
}
|
|
9316
|
-
/**
|
|
9317
|
-
* listenForConnectionChanges - Adds an event listener fired on browser going online or offline
|
|
9318
|
-
*/
|
|
9319
|
-
function addConnectionEventListeners(cb) {
|
|
9320
|
-
if (typeof window !== 'undefined' && window.addEventListener) {
|
|
9321
|
-
window.addEventListener('offline', cb);
|
|
9322
|
-
window.addEventListener('online', cb);
|
|
9323
|
-
}
|
|
9324
|
-
}
|
|
9325
|
-
function removeConnectionEventListeners(cb) {
|
|
9326
|
-
if (typeof window !== 'undefined' && window.removeEventListener) {
|
|
9327
|
-
window.removeEventListener('offline', cb);
|
|
9328
|
-
window.removeEventListener('online', cb);
|
|
9329
|
-
}
|
|
9330
|
-
}
|
|
9651
|
+
}
|
|
9652
|
+
catch (err) {
|
|
9653
|
+
logger('error', 'Failed to decode a message. Check whether the Proto models match.', { event: e, error: err });
|
|
9654
|
+
}
|
|
9655
|
+
});
|
|
9656
|
+
return ws;
|
|
9657
|
+
};
|
|
9658
|
+
|
|
9659
|
+
/**
|
|
9660
|
+
* Creates a new promise with resolvers.
|
|
9661
|
+
*
|
|
9662
|
+
* Based on:
|
|
9663
|
+
* - https://github.com/tc39/proposal-promise-with-resolvers/blob/main/polyfills.js
|
|
9664
|
+
*/
|
|
9665
|
+
const promiseWithResolvers = () => {
|
|
9666
|
+
let resolve;
|
|
9667
|
+
let reject;
|
|
9668
|
+
const promise = new Promise((_resolve, _reject) => {
|
|
9669
|
+
resolve = _resolve;
|
|
9670
|
+
reject = _reject;
|
|
9671
|
+
});
|
|
9672
|
+
let isResolved = false;
|
|
9673
|
+
let isRejected = false;
|
|
9674
|
+
const resolver = (value) => {
|
|
9675
|
+
isResolved = true;
|
|
9676
|
+
resolve(value);
|
|
9677
|
+
};
|
|
9678
|
+
const rejecter = (reason) => {
|
|
9679
|
+
isRejected = true;
|
|
9680
|
+
reject(reason);
|
|
9681
|
+
};
|
|
9682
|
+
return {
|
|
9683
|
+
promise,
|
|
9684
|
+
resolve: resolver,
|
|
9685
|
+
reject: rejecter,
|
|
9686
|
+
isResolved,
|
|
9687
|
+
isRejected,
|
|
9688
|
+
};
|
|
9689
|
+
};
|
|
9331
9690
|
|
|
9332
9691
|
/**
|
|
9333
9692
|
* The client used for exchanging information with the SFU.
|
|
@@ -9335,133 +9694,224 @@ function removeConnectionEventListeners(cb) {
|
|
|
9335
9694
|
class StreamSfuClient {
|
|
9336
9695
|
/**
|
|
9337
9696
|
* Constructs a new SFU client.
|
|
9338
|
-
*
|
|
9339
|
-
* @param dispatcher the event dispatcher to use.
|
|
9340
|
-
* @param sfuServer the SFU server to connect to.
|
|
9341
|
-
* @param token the JWT token to use for authentication.
|
|
9342
|
-
* @param sessionId the `sessionId` of the currently connected participant.
|
|
9343
9697
|
*/
|
|
9344
|
-
constructor({ dispatcher,
|
|
9698
|
+
constructor({ dispatcher, credentials, sessionId, logTag, joinResponseTimeout = 5000, onSignalClose, }) {
|
|
9345
9699
|
/**
|
|
9346
9700
|
* A buffer for ICE Candidates that are received before
|
|
9347
|
-
* the
|
|
9701
|
+
* the Publisher and Subscriber Peer Connections are ready to handle them.
|
|
9348
9702
|
*/
|
|
9349
9703
|
this.iceTrickleBuffer = new IceTrickleBuffer();
|
|
9350
9704
|
/**
|
|
9351
|
-
*
|
|
9352
|
-
*
|
|
9353
|
-
*/
|
|
9354
|
-
this.isMigratingAway = false;
|
|
9355
|
-
/**
|
|
9356
|
-
* A flag indicating that the client connection is broken for the current
|
|
9357
|
-
* client and that a fast-reconnect with a new client should be attempted.
|
|
9705
|
+
* Flag to indicate if the client is in the process of leaving the call.
|
|
9706
|
+
* This is set to `true` when the user initiates the leave process.
|
|
9358
9707
|
*/
|
|
9359
|
-
this.
|
|
9708
|
+
this.isLeaving = false;
|
|
9360
9709
|
this.pingIntervalInMs = 10 * 1000;
|
|
9361
9710
|
this.unhealthyTimeoutInMs = this.pingIntervalInMs + 5 * 1000;
|
|
9362
|
-
this.
|
|
9363
|
-
|
|
9364
|
-
|
|
9711
|
+
this.restoreWebSocketConcurrencyTag = Symbol('recoverWebSocket');
|
|
9712
|
+
/**
|
|
9713
|
+
* Promise that resolves when the JoinResponse is received.
|
|
9714
|
+
* Rejects after a certain threshold if the response is not received.
|
|
9715
|
+
*/
|
|
9716
|
+
this.joinResponseTask = promiseWithResolvers();
|
|
9717
|
+
/**
|
|
9718
|
+
* A controller to abort the current requests.
|
|
9719
|
+
*/
|
|
9720
|
+
this.abortController = new AbortController();
|
|
9721
|
+
this.createWebSocket = () => {
|
|
9722
|
+
this.signalWs = createWebSocketSignalChannel({
|
|
9723
|
+
logTag: this.logTag,
|
|
9724
|
+
endpoint: `${this.credentials.server.ws_endpoint}?tag=${this.logTag}`,
|
|
9725
|
+
onMessage: (message) => {
|
|
9726
|
+
this.lastMessageTimestamp = new Date();
|
|
9727
|
+
this.scheduleConnectionCheck();
|
|
9728
|
+
this.dispatcher.dispatch(message, this.logTag);
|
|
9729
|
+
},
|
|
9730
|
+
});
|
|
9731
|
+
this.signalWs.addEventListener('close', this.handleWebSocketClose);
|
|
9732
|
+
this.signalWs.addEventListener('error', this.restoreWebSocket);
|
|
9733
|
+
this.signalReady = new Promise((resolve) => {
|
|
9734
|
+
const onOpen = () => {
|
|
9735
|
+
this.signalWs.removeEventListener('open', onOpen);
|
|
9736
|
+
resolve(this.signalWs);
|
|
9737
|
+
};
|
|
9738
|
+
this.signalWs.addEventListener('open', onOpen);
|
|
9739
|
+
});
|
|
9740
|
+
};
|
|
9741
|
+
this.cleanUpWebSocket = () => {
|
|
9742
|
+
this.signalWs.removeEventListener('error', this.restoreWebSocket);
|
|
9743
|
+
this.signalWs.removeEventListener('close', this.handleWebSocketClose);
|
|
9744
|
+
};
|
|
9745
|
+
this.restoreWebSocket = () => {
|
|
9746
|
+
withoutConcurrency(this.restoreWebSocketConcurrencyTag, async () => {
|
|
9747
|
+
this.logger('debug', 'Restoring SFU WS connection');
|
|
9748
|
+
this.cleanUpWebSocket();
|
|
9749
|
+
await sleep(500);
|
|
9750
|
+
this.createWebSocket();
|
|
9751
|
+
}).catch((err) => this.logger('debug', `Can't restore WS connection`, err));
|
|
9752
|
+
};
|
|
9753
|
+
this.handleWebSocketClose = (e) => {
|
|
9754
|
+
this.signalWs.removeEventListener('close', this.handleWebSocketClose);
|
|
9755
|
+
clearInterval(this.keepAliveInterval);
|
|
9756
|
+
clearTimeout(this.connectionCheckTimeout);
|
|
9757
|
+
if (this.onSignalClose) {
|
|
9758
|
+
this.onSignalClose(e);
|
|
9759
|
+
}
|
|
9760
|
+
};
|
|
9761
|
+
this.close = (code = StreamSfuClient.NORMAL_CLOSURE, reason) => {
|
|
9762
|
+
if (this.signalWs.readyState === WebSocket.OPEN) {
|
|
9763
|
+
this.logger('debug', `Closing SFU WS connection: ${code} - ${reason}`);
|
|
9365
9764
|
this.signalWs.close(code, `js-client: ${reason}`);
|
|
9765
|
+
this.cleanUpWebSocket();
|
|
9366
9766
|
}
|
|
9767
|
+
this.dispose();
|
|
9768
|
+
};
|
|
9769
|
+
this.dispose = () => {
|
|
9770
|
+
this.logger('debug', 'Disposing SFU client');
|
|
9367
9771
|
this.unsubscribeIceTrickle();
|
|
9368
9772
|
clearInterval(this.keepAliveInterval);
|
|
9369
9773
|
clearTimeout(this.connectionCheckTimeout);
|
|
9774
|
+
clearTimeout(this.migrateAwayTimeout);
|
|
9775
|
+
this.abortController.abort();
|
|
9776
|
+
this.migrationTask?.resolve();
|
|
9777
|
+
};
|
|
9778
|
+
this.leaveAndClose = async (reason) => {
|
|
9779
|
+
await this.joinResponseTask.promise;
|
|
9780
|
+
try {
|
|
9781
|
+
this.isLeaving = true;
|
|
9782
|
+
await this.notifyLeave(reason);
|
|
9783
|
+
}
|
|
9784
|
+
catch (err) {
|
|
9785
|
+
this.logger('debug', 'Error notifying SFU about leaving call', err);
|
|
9786
|
+
}
|
|
9787
|
+
this.close(StreamSfuClient.NORMAL_CLOSURE, reason.substring(0, 115));
|
|
9370
9788
|
};
|
|
9371
|
-
this.updateSubscriptions = async (
|
|
9372
|
-
|
|
9373
|
-
|
|
9374
|
-
tracks: subscriptions,
|
|
9375
|
-
}), this.logger, 'debug');
|
|
9789
|
+
this.updateSubscriptions = async (tracks) => {
|
|
9790
|
+
await this.joinResponseTask.promise;
|
|
9791
|
+
return retryable(() => this.rpc.updateSubscriptions({ sessionId: this.sessionId, tracks }), this.abortController.signal);
|
|
9376
9792
|
};
|
|
9377
9793
|
this.setPublisher = async (data) => {
|
|
9378
|
-
|
|
9379
|
-
|
|
9380
|
-
sessionId: this.sessionId,
|
|
9381
|
-
}), this.logger);
|
|
9794
|
+
await this.joinResponseTask.promise;
|
|
9795
|
+
return retryable(() => this.rpc.setPublisher({ ...data, sessionId: this.sessionId }), this.abortController.signal);
|
|
9382
9796
|
};
|
|
9383
9797
|
this.sendAnswer = async (data) => {
|
|
9384
|
-
|
|
9385
|
-
|
|
9386
|
-
sessionId: this.sessionId,
|
|
9387
|
-
}), this.logger);
|
|
9798
|
+
await this.joinResponseTask.promise;
|
|
9799
|
+
return retryable(() => this.rpc.sendAnswer({ ...data, sessionId: this.sessionId }), this.abortController.signal);
|
|
9388
9800
|
};
|
|
9389
9801
|
this.iceTrickle = async (data) => {
|
|
9390
|
-
|
|
9391
|
-
|
|
9392
|
-
sessionId: this.sessionId,
|
|
9393
|
-
}), this.logger);
|
|
9802
|
+
await this.joinResponseTask.promise;
|
|
9803
|
+
return retryable(() => this.rpc.iceTrickle({ ...data, sessionId: this.sessionId }), this.abortController.signal);
|
|
9394
9804
|
};
|
|
9395
9805
|
this.iceRestart = async (data) => {
|
|
9396
|
-
|
|
9397
|
-
|
|
9398
|
-
sessionId: this.sessionId,
|
|
9399
|
-
}), this.logger);
|
|
9806
|
+
await this.joinResponseTask.promise;
|
|
9807
|
+
return retryable(() => this.rpc.iceRestart({ ...data, sessionId: this.sessionId }), this.abortController.signal);
|
|
9400
9808
|
};
|
|
9401
9809
|
this.updateMuteState = async (trackType, muted) => {
|
|
9402
|
-
|
|
9403
|
-
|
|
9404
|
-
{
|
|
9405
|
-
trackType,
|
|
9406
|
-
muted,
|
|
9407
|
-
},
|
|
9408
|
-
],
|
|
9409
|
-
});
|
|
9810
|
+
await this.joinResponseTask.promise;
|
|
9811
|
+
return this.updateMuteStates({ muteStates: [{ trackType, muted }] });
|
|
9410
9812
|
};
|
|
9411
9813
|
this.updateMuteStates = async (data) => {
|
|
9412
|
-
|
|
9413
|
-
|
|
9414
|
-
sessionId: this.sessionId,
|
|
9415
|
-
}), this.logger);
|
|
9814
|
+
await this.joinResponseTask.promise;
|
|
9815
|
+
return retryable(() => this.rpc.updateMuteStates({ ...data, sessionId: this.sessionId }), this.abortController.signal);
|
|
9416
9816
|
};
|
|
9417
9817
|
this.sendStats = async (stats) => {
|
|
9418
|
-
|
|
9419
|
-
|
|
9420
|
-
sessionId: this.sessionId,
|
|
9421
|
-
}), this.logger, 'debug');
|
|
9818
|
+
await this.joinResponseTask.promise;
|
|
9819
|
+
return retryable(() => this.rpc.sendStats({ ...stats, sessionId: this.sessionId }), this.abortController.signal);
|
|
9422
9820
|
};
|
|
9423
9821
|
this.startNoiseCancellation = async () => {
|
|
9424
|
-
|
|
9425
|
-
|
|
9426
|
-
}), this.logger);
|
|
9822
|
+
await this.joinResponseTask.promise;
|
|
9823
|
+
return retryable(() => this.rpc.startNoiseCancellation({ sessionId: this.sessionId }), this.abortController.signal);
|
|
9427
9824
|
};
|
|
9428
9825
|
this.stopNoiseCancellation = async () => {
|
|
9429
|
-
|
|
9430
|
-
|
|
9431
|
-
|
|
9826
|
+
await this.joinResponseTask.promise;
|
|
9827
|
+
return retryable(() => this.rpc.stopNoiseCancellation({ sessionId: this.sessionId }), this.abortController.signal);
|
|
9828
|
+
};
|
|
9829
|
+
this.enterMigration = async (opts = {}) => {
|
|
9830
|
+
this.isLeaving = true;
|
|
9831
|
+
const { timeout = 7 * 1000 } = opts;
|
|
9832
|
+
this.migrationTask?.reject(new Error('Cancelled previous migration'));
|
|
9833
|
+
const task = (this.migrationTask = promiseWithResolvers());
|
|
9834
|
+
const unsubscribe = this.dispatcher.on('participantMigrationComplete', () => {
|
|
9835
|
+
unsubscribe();
|
|
9836
|
+
clearTimeout(this.migrateAwayTimeout);
|
|
9837
|
+
task.resolve();
|
|
9838
|
+
});
|
|
9839
|
+
this.migrateAwayTimeout = setTimeout(() => {
|
|
9840
|
+
unsubscribe();
|
|
9841
|
+
task.reject(new Error(`Migration (${this.logTag}) failed to complete in ${timeout}ms`));
|
|
9842
|
+
}, timeout);
|
|
9843
|
+
return task.promise;
|
|
9432
9844
|
};
|
|
9433
9845
|
this.join = async (data) => {
|
|
9434
|
-
|
|
9435
|
-
|
|
9436
|
-
|
|
9437
|
-
|
|
9846
|
+
// wait for the signal web socket to be ready before sending "joinRequest"
|
|
9847
|
+
await this.signalReady;
|
|
9848
|
+
if (this.joinResponseTask.isResolved || this.joinResponseTask.isRejected) {
|
|
9849
|
+
// we need to lock the RPC requests until we receive a JoinResponse.
|
|
9850
|
+
// that's why we have this primitive lock mechanism.
|
|
9851
|
+
// the client starts with already initialized joinResponseTask,
|
|
9852
|
+
// and this code creates a new one for the next join request.
|
|
9853
|
+
this.joinResponseTask = promiseWithResolvers();
|
|
9854
|
+
}
|
|
9855
|
+
// capture a reference to the current joinResponseTask as it might
|
|
9856
|
+
// be replaced with a new one in case a second join request is made
|
|
9857
|
+
const current = this.joinResponseTask;
|
|
9858
|
+
let timeoutId;
|
|
9859
|
+
const unsubscribe = this.dispatcher.on('joinResponse', (joinResponse) => {
|
|
9860
|
+
this.logger('debug', 'Received joinResponse', joinResponse);
|
|
9861
|
+
clearTimeout(timeoutId);
|
|
9862
|
+
unsubscribe();
|
|
9863
|
+
this.keepAlive();
|
|
9864
|
+
current.resolve(joinResponse);
|
|
9438
9865
|
});
|
|
9439
|
-
|
|
9866
|
+
timeoutId = setTimeout(() => {
|
|
9867
|
+
unsubscribe();
|
|
9868
|
+
current.reject(new Error('Waiting for "joinResponse" has timed out'));
|
|
9869
|
+
}, this.joinResponseTimeout);
|
|
9870
|
+
await this.send(SfuRequest.create({
|
|
9440
9871
|
requestPayload: {
|
|
9441
9872
|
oneofKind: 'joinRequest',
|
|
9442
|
-
joinRequest
|
|
9873
|
+
joinRequest: JoinRequest.create({
|
|
9874
|
+
...data,
|
|
9875
|
+
sessionId: this.sessionId,
|
|
9876
|
+
token: this.credentials.token,
|
|
9877
|
+
}),
|
|
9878
|
+
},
|
|
9879
|
+
}));
|
|
9880
|
+
return current.promise;
|
|
9881
|
+
};
|
|
9882
|
+
this.ping = async () => {
|
|
9883
|
+
return this.send(SfuRequest.create({
|
|
9884
|
+
requestPayload: {
|
|
9885
|
+
oneofKind: 'healthCheckRequest',
|
|
9886
|
+
healthCheckRequest: {},
|
|
9887
|
+
},
|
|
9888
|
+
}));
|
|
9889
|
+
};
|
|
9890
|
+
this.notifyLeave = async (reason) => {
|
|
9891
|
+
return this.send(SfuRequest.create({
|
|
9892
|
+
requestPayload: {
|
|
9893
|
+
oneofKind: 'leaveCallRequest',
|
|
9894
|
+
leaveCallRequest: {
|
|
9895
|
+
sessionId: this.sessionId,
|
|
9896
|
+
reason,
|
|
9897
|
+
},
|
|
9443
9898
|
},
|
|
9444
9899
|
}));
|
|
9445
9900
|
};
|
|
9446
9901
|
this.send = async (message) => {
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
|
|
9450
|
-
this.logger('debug',
|
|
9451
|
-
|
|
9452
|
-
}
|
|
9902
|
+
await this.signalReady; // wait for the signal ws to be open
|
|
9903
|
+
const msgJson = SfuRequest.toJson(message);
|
|
9904
|
+
if (this.signalWs.readyState !== WebSocket.OPEN) {
|
|
9905
|
+
this.logger('debug', 'Signal WS is not open. Skipping message', msgJson);
|
|
9906
|
+
return;
|
|
9907
|
+
}
|
|
9908
|
+
this.logger('debug', `Sending message to: ${this.edgeName}`, msgJson);
|
|
9909
|
+
this.signalWs.send(SfuRequest.toBinary(message));
|
|
9453
9910
|
};
|
|
9454
9911
|
this.keepAlive = () => {
|
|
9455
9912
|
clearInterval(this.keepAliveInterval);
|
|
9456
9913
|
this.keepAliveInterval = setInterval(() => {
|
|
9457
|
-
this.
|
|
9458
|
-
const message = SfuRequest.create({
|
|
9459
|
-
requestPayload: {
|
|
9460
|
-
oneofKind: 'healthCheckRequest',
|
|
9461
|
-
healthCheckRequest: {},
|
|
9462
|
-
},
|
|
9463
|
-
});
|
|
9464
|
-
this.send(message).catch((e) => {
|
|
9914
|
+
this.ping().catch((e) => {
|
|
9465
9915
|
this.logger('error', 'Error sending healthCheckRequest to SFU', e);
|
|
9466
9916
|
});
|
|
9467
9917
|
}, this.pingIntervalInMs);
|
|
@@ -9477,53 +9927,37 @@ class StreamSfuClient {
|
|
|
9477
9927
|
}
|
|
9478
9928
|
}, this.unhealthyTimeoutInMs);
|
|
9479
9929
|
};
|
|
9930
|
+
this.dispatcher = dispatcher;
|
|
9480
9931
|
this.sessionId = sessionId || generateUUIDv4();
|
|
9481
|
-
this.
|
|
9482
|
-
this.
|
|
9483
|
-
|
|
9484
|
-
this.
|
|
9485
|
-
|
|
9486
|
-
|
|
9487
|
-
|
|
9488
|
-
input,
|
|
9489
|
-
options,
|
|
9490
|
-
});
|
|
9491
|
-
return next(method, input, options);
|
|
9492
|
-
},
|
|
9493
|
-
};
|
|
9932
|
+
this.onSignalClose = onSignalClose;
|
|
9933
|
+
this.credentials = credentials;
|
|
9934
|
+
const { server, token } = credentials;
|
|
9935
|
+
this.edgeName = server.edge_name;
|
|
9936
|
+
this.joinResponseTimeout = joinResponseTimeout;
|
|
9937
|
+
this.logTag = logTag;
|
|
9938
|
+
this.logger = getLogger(['sfu-client', logTag]);
|
|
9494
9939
|
this.rpc = createSignalClient({
|
|
9495
|
-
baseUrl:
|
|
9940
|
+
baseUrl: server.url,
|
|
9496
9941
|
interceptors: [
|
|
9497
9942
|
withHeaders({
|
|
9498
9943
|
Authorization: `Bearer ${token}`,
|
|
9499
9944
|
}),
|
|
9500
|
-
|
|
9501
|
-
],
|
|
9945
|
+
getLogLevel() === 'trace' && withRequestLogger(this.logger, 'trace'),
|
|
9946
|
+
].filter((v) => !!v),
|
|
9502
9947
|
});
|
|
9503
9948
|
// Special handling for the ICETrickle kind of events.
|
|
9504
|
-
//
|
|
9505
|
-
// connection is established
|
|
9506
|
-
//
|
|
9949
|
+
// The SFU might trigger these events before the initial RTC
|
|
9950
|
+
// connection is established or "JoinResponse" received.
|
|
9951
|
+
// In that case, those events (ICE candidates) need to be buffered
|
|
9952
|
+
// and later added to the appropriate PeerConnection
|
|
9507
9953
|
// once the remoteDescription is known and set.
|
|
9508
9954
|
this.unsubscribeIceTrickle = dispatcher.on('iceTrickle', (iceTrickle) => {
|
|
9509
9955
|
this.iceTrickleBuffer.push(iceTrickle);
|
|
9510
9956
|
});
|
|
9511
|
-
this.
|
|
9512
|
-
|
|
9513
|
-
|
|
9514
|
-
|
|
9515
|
-
this.scheduleConnectionCheck();
|
|
9516
|
-
dispatcher.dispatch(message);
|
|
9517
|
-
},
|
|
9518
|
-
});
|
|
9519
|
-
this.signalReady = new Promise((resolve) => {
|
|
9520
|
-
const onOpen = () => {
|
|
9521
|
-
this.signalWs.removeEventListener('open', onOpen);
|
|
9522
|
-
this.keepAlive();
|
|
9523
|
-
resolve(this.signalWs);
|
|
9524
|
-
};
|
|
9525
|
-
this.signalWs.addEventListener('open', onOpen);
|
|
9526
|
-
});
|
|
9957
|
+
this.createWebSocket();
|
|
9958
|
+
}
|
|
9959
|
+
get isHealthy() {
|
|
9960
|
+
return this.signalWs.readyState === WebSocket.OPEN;
|
|
9527
9961
|
}
|
|
9528
9962
|
}
|
|
9529
9963
|
/**
|
|
@@ -9536,45 +9970,15 @@ StreamSfuClient.NORMAL_CLOSURE = 1000;
|
|
|
9536
9970
|
* a certain amount of time (`connectionCheckTimeout`).
|
|
9537
9971
|
*/
|
|
9538
9972
|
StreamSfuClient.ERROR_CONNECTION_UNHEALTHY = 4001;
|
|
9539
|
-
|
|
9540
|
-
|
|
9541
|
-
|
|
9542
|
-
|
|
9543
|
-
|
|
9544
|
-
|
|
9545
|
-
|
|
9546
|
-
|
|
9547
|
-
|
|
9548
|
-
* the RPC until it succeeds or the maximum number of retries is reached.
|
|
9549
|
-
*
|
|
9550
|
-
* Between each retry, there would be a random delay in order to avoid
|
|
9551
|
-
* request bursts towards the SFU.
|
|
9552
|
-
*
|
|
9553
|
-
* @param rpc the closure around the RPC call to execute.
|
|
9554
|
-
* @param logger a logger instance to use.
|
|
9555
|
-
* @param <I> the type of the request object.
|
|
9556
|
-
* @param <O> the type of the response object.
|
|
9557
|
-
*/
|
|
9558
|
-
const retryable = async (rpc, logger, level = 'error') => {
|
|
9559
|
-
let retryAttempt = 0;
|
|
9560
|
-
let rpcCallResult;
|
|
9561
|
-
do {
|
|
9562
|
-
// don't delay the first invocation
|
|
9563
|
-
if (retryAttempt > 0) {
|
|
9564
|
-
await sleep(retryInterval(retryAttempt));
|
|
9565
|
-
}
|
|
9566
|
-
rpcCallResult = await rpc();
|
|
9567
|
-
// if the RPC call failed, log the error and retry
|
|
9568
|
-
if (rpcCallResult.response.error) {
|
|
9569
|
-
logger(level, `SFU RPC Error (${rpcCallResult.method.name}):`, rpcCallResult.response.error);
|
|
9570
|
-
}
|
|
9571
|
-
retryAttempt++;
|
|
9572
|
-
} while (rpcCallResult.response.error?.shouldRetry &&
|
|
9573
|
-
retryAttempt < MAX_RETRIES);
|
|
9574
|
-
if (rpcCallResult.response.error) {
|
|
9575
|
-
throw rpcCallResult.response.error;
|
|
9576
|
-
}
|
|
9577
|
-
return rpcCallResult;
|
|
9973
|
+
|
|
9974
|
+
const toRtcConfiguration = (config) => {
|
|
9975
|
+
return {
|
|
9976
|
+
iceServers: config.map((ice) => ({
|
|
9977
|
+
urls: ice.urls,
|
|
9978
|
+
username: ice.username,
|
|
9979
|
+
credential: ice.password,
|
|
9980
|
+
})),
|
|
9981
|
+
};
|
|
9578
9982
|
};
|
|
9579
9983
|
|
|
9580
9984
|
/**
|
|
@@ -9729,9 +10133,10 @@ const watchSfuErrorReports = (dispatcher) => {
|
|
|
9729
10133
|
return dispatcher.on('error', (e) => {
|
|
9730
10134
|
if (!e.error)
|
|
9731
10135
|
return;
|
|
9732
|
-
const { error } = e;
|
|
10136
|
+
const { error, reconnectStrategy } = e;
|
|
9733
10137
|
logger$1('error', 'SFU reported error', {
|
|
9734
10138
|
code: ErrorCode[error.code],
|
|
10139
|
+
reconnectStrategy: WebsocketReconnectStrategy[reconnectStrategy],
|
|
9735
10140
|
message: error.message,
|
|
9736
10141
|
shouldRetry: error.shouldRetry,
|
|
9737
10142
|
});
|
|
@@ -9747,6 +10152,17 @@ const watchPinsUpdated = (state) => {
|
|
|
9747
10152
|
state.setServerSidePins(pins);
|
|
9748
10153
|
};
|
|
9749
10154
|
};
|
|
10155
|
+
/**
|
|
10156
|
+
* Watches for `callEnded` events.
|
|
10157
|
+
*/
|
|
10158
|
+
const watchSfuCallEnded = (call) => {
|
|
10159
|
+
return call.on('callEnded', (e) => {
|
|
10160
|
+
const reason = CallEndedReason[e.reason];
|
|
10161
|
+
call.leave({ reason }).catch((err) => {
|
|
10162
|
+
logger$1('error', 'Failed to leave call after call ended by the SFU', err);
|
|
10163
|
+
});
|
|
10164
|
+
});
|
|
10165
|
+
};
|
|
9750
10166
|
|
|
9751
10167
|
/**
|
|
9752
10168
|
* An event handler that handles soft mutes.
|
|
@@ -9768,12 +10184,13 @@ const handleRemoteSoftMute = (call) => {
|
|
|
9768
10184
|
else if (type === TrackType.AUDIO) {
|
|
9769
10185
|
await call.microphone.disable();
|
|
9770
10186
|
}
|
|
10187
|
+
else if (type === TrackType.SCREEN_SHARE ||
|
|
10188
|
+
type === TrackType.SCREEN_SHARE_AUDIO) {
|
|
10189
|
+
await call.screenShare.disable();
|
|
10190
|
+
}
|
|
9771
10191
|
else {
|
|
9772
10192
|
logger('warn', 'Unsupported track type to soft mute', TrackType[type]);
|
|
9773
10193
|
}
|
|
9774
|
-
if (call.publisher?.isPublishing(type)) {
|
|
9775
|
-
await call.stopPublish(type);
|
|
9776
|
-
}
|
|
9777
10194
|
}
|
|
9778
10195
|
catch (error) {
|
|
9779
10196
|
logger('error', 'Failed to stop publishing', error);
|
|
@@ -9794,11 +10211,12 @@ const watchParticipantJoined = (state) => {
|
|
|
9794
10211
|
// potential duplicate events from the SFU.
|
|
9795
10212
|
//
|
|
9796
10213
|
// Although the SFU should not send duplicate events, we have seen
|
|
9797
|
-
// some race conditions in the past during the `join-flow
|
|
9798
|
-
//
|
|
10214
|
+
// some race conditions in the past during the `join-flow`.
|
|
10215
|
+
// The SFU would send participant info as part of the `join`
|
|
9799
10216
|
// response and then follow up with a `participantJoined` event for
|
|
9800
10217
|
// already announced participants.
|
|
9801
|
-
state
|
|
10218
|
+
const orphanedTracks = reconcileOrphanedTracks(state, participant);
|
|
10219
|
+
state.updateOrAddParticipant(participant.sessionId, Object.assign(participant, orphanedTracks, {
|
|
9802
10220
|
viewportVisibilityState: {
|
|
9803
10221
|
videoTrack: exports.VisibilityState.UNKNOWN,
|
|
9804
10222
|
screenShareTrack: exports.VisibilityState.UNKNOWN,
|
|
@@ -9834,12 +10252,14 @@ const watchParticipantUpdated = (state) => {
|
|
|
9834
10252
|
*/
|
|
9835
10253
|
const watchTrackPublished = (state) => {
|
|
9836
10254
|
return function onTrackPublished(e) {
|
|
9837
|
-
const { type, sessionId
|
|
10255
|
+
const { type, sessionId } = e;
|
|
9838
10256
|
// An optimization for large calls.
|
|
9839
10257
|
// After a certain threshold, the SFU would stop emitting `participantJoined`
|
|
9840
10258
|
// events, and instead, it would only provide the participant's information
|
|
9841
10259
|
// once they start publishing a track.
|
|
9842
|
-
if (participant) {
|
|
10260
|
+
if (e.participant) {
|
|
10261
|
+
const orphanedTracks = reconcileOrphanedTracks(state, e.participant);
|
|
10262
|
+
const participant = Object.assign(e.participant, orphanedTracks);
|
|
9843
10263
|
state.updateOrAddParticipant(sessionId, participant);
|
|
9844
10264
|
}
|
|
9845
10265
|
else {
|
|
@@ -9855,9 +10275,11 @@ const watchTrackPublished = (state) => {
|
|
|
9855
10275
|
*/
|
|
9856
10276
|
const watchTrackUnpublished = (state) => {
|
|
9857
10277
|
return function onTrackUnpublished(e) {
|
|
9858
|
-
const { type, sessionId
|
|
10278
|
+
const { type, sessionId } = e;
|
|
9859
10279
|
// An optimization for large calls. See `watchTrackPublished`.
|
|
9860
|
-
if (participant) {
|
|
10280
|
+
if (e.participant) {
|
|
10281
|
+
const orphanedTracks = reconcileOrphanedTracks(state, e.participant);
|
|
10282
|
+
const participant = Object.assign(e.participant, orphanedTracks);
|
|
9861
10283
|
state.updateOrAddParticipant(sessionId, participant);
|
|
9862
10284
|
}
|
|
9863
10285
|
else {
|
|
@@ -9868,6 +10290,25 @@ const watchTrackUnpublished = (state) => {
|
|
|
9868
10290
|
};
|
|
9869
10291
|
};
|
|
9870
10292
|
const unique = (v, i, arr) => arr.indexOf(v) === i;
|
|
10293
|
+
/**
|
|
10294
|
+
* Reconciles orphaned tracks (if any) for the given participant.
|
|
10295
|
+
*
|
|
10296
|
+
* @param state the call state.
|
|
10297
|
+
* @param participant the participant.
|
|
10298
|
+
*/
|
|
10299
|
+
const reconcileOrphanedTracks = (state, participant) => {
|
|
10300
|
+
const orphanTracks = state.takeOrphanedTracks(participant.trackLookupPrefix);
|
|
10301
|
+
if (!orphanTracks.length)
|
|
10302
|
+
return;
|
|
10303
|
+
const reconciledTracks = {};
|
|
10304
|
+
for (const orphan of orphanTracks) {
|
|
10305
|
+
const key = trackTypeToParticipantStreamKey(orphan.trackType);
|
|
10306
|
+
if (!key)
|
|
10307
|
+
continue;
|
|
10308
|
+
reconciledTracks[key] = orphan.track;
|
|
10309
|
+
}
|
|
10310
|
+
return reconciledTracks;
|
|
10311
|
+
};
|
|
9871
10312
|
|
|
9872
10313
|
/**
|
|
9873
10314
|
* Watches for `dominantSpeakerChanged` events.
|
|
@@ -9916,12 +10357,13 @@ const watchAudioLevelChanged = (dispatcher, state) => {
|
|
|
9916
10357
|
* Registers the default event handlers for a call during its lifecycle.
|
|
9917
10358
|
*
|
|
9918
10359
|
* @param call the call to register event handlers for.
|
|
9919
|
-
* @param state the call state.
|
|
9920
10360
|
* @param dispatcher the dispatcher.
|
|
9921
10361
|
*/
|
|
9922
|
-
const registerEventHandlers = (call,
|
|
10362
|
+
const registerEventHandlers = (call, dispatcher) => {
|
|
10363
|
+
const state = call.state;
|
|
9923
10364
|
const eventHandlers = [
|
|
9924
10365
|
call.on('call.ended', watchCallEnded(call)),
|
|
10366
|
+
watchSfuCallEnded(call),
|
|
9925
10367
|
watchLiveEnded(dispatcher, call),
|
|
9926
10368
|
watchSfuErrorReports(dispatcher),
|
|
9927
10369
|
watchChangePublishQuality(dispatcher, call),
|
|
@@ -9965,48 +10407,6 @@ const registerRingingCallEventHandlers = (call) => {
|
|
|
9965
10407
|
};
|
|
9966
10408
|
};
|
|
9967
10409
|
|
|
9968
|
-
/**
|
|
9969
|
-
* Collects all necessary information to join a call, talks to the coordinator
|
|
9970
|
-
* and returns the necessary information to join the call.
|
|
9971
|
-
*
|
|
9972
|
-
* @param httpClient the http client to use.
|
|
9973
|
-
* @param type the type of the call.
|
|
9974
|
-
* @param id the id of the call.
|
|
9975
|
-
* @param data the data for the call.
|
|
9976
|
-
*/
|
|
9977
|
-
const join = async (httpClient, type, id, data) => {
|
|
9978
|
-
const { call, credentials, members, own_capabilities, stats_options } = await doJoin(httpClient, type, id, data);
|
|
9979
|
-
return {
|
|
9980
|
-
connectionConfig: toRtcConfiguration(credentials.ice_servers),
|
|
9981
|
-
sfuServer: credentials.server,
|
|
9982
|
-
token: credentials.token,
|
|
9983
|
-
metadata: call,
|
|
9984
|
-
members,
|
|
9985
|
-
ownCapabilities: own_capabilities,
|
|
9986
|
-
statsOptions: stats_options,
|
|
9987
|
-
};
|
|
9988
|
-
};
|
|
9989
|
-
const doJoin = async (httpClient, type, id, data) => {
|
|
9990
|
-
const location = await httpClient.getLocationHint();
|
|
9991
|
-
const request = {
|
|
9992
|
-
...data,
|
|
9993
|
-
location,
|
|
9994
|
-
};
|
|
9995
|
-
return httpClient.post(`/call/${type}/${id}/join`, request);
|
|
9996
|
-
};
|
|
9997
|
-
const toRtcConfiguration = (config) => {
|
|
9998
|
-
if (!config || config.length === 0)
|
|
9999
|
-
return undefined;
|
|
10000
|
-
const rtcConfig = {
|
|
10001
|
-
iceServers: config.map((ice) => ({
|
|
10002
|
-
urls: ice.urls,
|
|
10003
|
-
username: ice.username,
|
|
10004
|
-
credential: ice.password,
|
|
10005
|
-
})),
|
|
10006
|
-
};
|
|
10007
|
-
return rtcConfig;
|
|
10008
|
-
};
|
|
10009
|
-
|
|
10010
10410
|
/**
|
|
10011
10411
|
* Flatten the stats report into an array of stats objects.
|
|
10012
10412
|
*
|
|
@@ -10286,6 +10686,7 @@ class SfuStatsReporter {
|
|
|
10286
10686
|
this.start = () => {
|
|
10287
10687
|
if (this.options.reporting_interval_ms <= 0)
|
|
10288
10688
|
return;
|
|
10689
|
+
clearInterval(this.intervalId);
|
|
10289
10690
|
this.intervalId = setInterval(() => {
|
|
10290
10691
|
this.run().catch((err) => {
|
|
10291
10692
|
this.logger('warn', 'Failed to report stats', err);
|
|
@@ -10969,7 +11370,7 @@ function lazy(factory) {
|
|
|
10969
11370
|
* Returns an Observable that emits the list of available devices
|
|
10970
11371
|
* that meet the given constraints.
|
|
10971
11372
|
*
|
|
10972
|
-
* @param
|
|
11373
|
+
* @param permission a BrowserPermission instance.
|
|
10973
11374
|
* @param kind the kind of devices to enumerate.
|
|
10974
11375
|
*/
|
|
10975
11376
|
const getDevices = (permission, kind) => {
|
|
@@ -11222,6 +11623,12 @@ class InputMediaDeviceManager {
|
|
|
11222
11623
|
listDevices() {
|
|
11223
11624
|
return this.getDevices();
|
|
11224
11625
|
}
|
|
11626
|
+
/**
|
|
11627
|
+
* Returns `true` when this device is in enabled state.
|
|
11628
|
+
*/
|
|
11629
|
+
get enabled() {
|
|
11630
|
+
return this.state.status === 'enabled';
|
|
11631
|
+
}
|
|
11225
11632
|
/**
|
|
11226
11633
|
* Starts stream.
|
|
11227
11634
|
*/
|
|
@@ -11344,7 +11751,7 @@ class InputMediaDeviceManager {
|
|
|
11344
11751
|
await this.applySettingsToStream();
|
|
11345
11752
|
}
|
|
11346
11753
|
async applySettingsToStream() {
|
|
11347
|
-
if (this.
|
|
11754
|
+
if (this.enabled) {
|
|
11348
11755
|
await this.muteStream();
|
|
11349
11756
|
await this.unmuteStream();
|
|
11350
11757
|
}
|
|
@@ -11481,19 +11888,22 @@ class InputMediaDeviceManager {
|
|
|
11481
11888
|
return output;
|
|
11482
11889
|
})
|
|
11483
11890
|
.then(chainWith(parent), (error) => {
|
|
11484
|
-
this.logger('warn', '
|
|
11891
|
+
this.logger('warn', 'Filter failed to start and will be ignored', error);
|
|
11485
11892
|
return parent;
|
|
11486
11893
|
}), rootStream);
|
|
11487
11894
|
}
|
|
11488
11895
|
if (this.call.state.callingState === exports.CallingState.JOINED) {
|
|
11489
11896
|
await this.publishStream(stream);
|
|
11490
11897
|
}
|
|
11898
|
+
else {
|
|
11899
|
+
this.logger('debug', 'Stream is not published as the call is not joined');
|
|
11900
|
+
}
|
|
11491
11901
|
if (this.state.mediaStream !== stream) {
|
|
11492
11902
|
this.state.setMediaStream(stream, await rootStream);
|
|
11493
11903
|
this.getTracks().forEach((track) => {
|
|
11494
11904
|
track.addEventListener('ended', async () => {
|
|
11495
11905
|
await this.statusChangeSettled();
|
|
11496
|
-
if (this.
|
|
11906
|
+
if (this.enabled) {
|
|
11497
11907
|
this.isTrackStoppedDueToTrackEnd = true;
|
|
11498
11908
|
setTimeout(() => {
|
|
11499
11909
|
this.isTrackStoppedDueToTrackEnd = false;
|
|
@@ -11786,7 +12196,7 @@ class CameraManager extends InputMediaDeviceManager {
|
|
|
11786
12196
|
this.logger('warn', 'could not apply target resolution', error);
|
|
11787
12197
|
}
|
|
11788
12198
|
}
|
|
11789
|
-
if (this.
|
|
12199
|
+
if (this.enabled) {
|
|
11790
12200
|
const { width, height } = this.state
|
|
11791
12201
|
.mediaStream.getVideoTracks()[0]
|
|
11792
12202
|
?.getSettings();
|
|
@@ -12090,7 +12500,7 @@ class MicrophoneManager extends InputMediaDeviceManager {
|
|
|
12090
12500
|
});
|
|
12091
12501
|
const registrationResult = this.registerFilter(noiseCancellation.toFilter());
|
|
12092
12502
|
this.noiseCancellationRegistration = registrationResult.registered;
|
|
12093
|
-
this.
|
|
12503
|
+
this.unregisterNoiseCancellation = registrationResult.unregister;
|
|
12094
12504
|
await this.noiseCancellationRegistration;
|
|
12095
12505
|
// handles an edge case where a noise cancellation is enabled after
|
|
12096
12506
|
// the participant as joined the call -> we immediately enable NC
|
|
@@ -12116,7 +12526,7 @@ class MicrophoneManager extends InputMediaDeviceManager {
|
|
|
12116
12526
|
if (isReactNative()) {
|
|
12117
12527
|
throw new Error('Noise cancellation is not supported in React Native');
|
|
12118
12528
|
}
|
|
12119
|
-
await (this.
|
|
12529
|
+
await (this.unregisterNoiseCancellation?.() ?? Promise.resolve())
|
|
12120
12530
|
.then(() => this.noiseCancellation?.disable())
|
|
12121
12531
|
.then(() => this.noiseCancellationChangeUnsubscribe?.())
|
|
12122
12532
|
.catch((err) => {
|
|
@@ -12498,9 +12908,17 @@ class Call {
|
|
|
12498
12908
|
*/
|
|
12499
12909
|
this.dispatcher = new Dispatcher();
|
|
12500
12910
|
this.trackSubscriptionsSubject = new rxjs.BehaviorSubject({ type: exports.DebounceType.MEDIUM, data: [] });
|
|
12911
|
+
this.sfuClientTag = 0;
|
|
12912
|
+
this.reconnectConcurrencyTag = Symbol('reconnectConcurrencyTag');
|
|
12501
12913
|
this.reconnectAttempts = 0;
|
|
12502
|
-
this.
|
|
12503
|
-
this.
|
|
12914
|
+
this.reconnectStrategy = WebsocketReconnectStrategy.UNSPECIFIED;
|
|
12915
|
+
this.fastReconnectDeadlineSeconds = 0;
|
|
12916
|
+
this.lastOfflineTimestamp = 0;
|
|
12917
|
+
// maintain the order of publishing tracks to restore them after a reconnection
|
|
12918
|
+
// it shouldn't contain duplicates
|
|
12919
|
+
this.trackPublishOrder = [];
|
|
12920
|
+
this.hasJoinedOnce = false;
|
|
12921
|
+
this.deviceSettingsAppliedOnce = false;
|
|
12504
12922
|
this.initialized = false;
|
|
12505
12923
|
this.joinLeaveConcurrencyTag = Symbol('joinLeaveConcurrencyTag');
|
|
12506
12924
|
/**
|
|
@@ -12510,6 +12928,42 @@ class Call {
|
|
|
12510
12928
|
*/
|
|
12511
12929
|
this.leaveCallHooks = new Set();
|
|
12512
12930
|
this.streamClientEventHandlers = new Map();
|
|
12931
|
+
this.handleOwnCapabilitiesUpdated = async (ownCapabilities) => {
|
|
12932
|
+
// update the permission context.
|
|
12933
|
+
this.permissionsContext.setPermissions(ownCapabilities);
|
|
12934
|
+
if (!this.publisher)
|
|
12935
|
+
return;
|
|
12936
|
+
// check if the user still has publishing permissions and stop publishing if not.
|
|
12937
|
+
const permissionToTrackType = {
|
|
12938
|
+
[OwnCapability.SEND_AUDIO]: TrackType.AUDIO,
|
|
12939
|
+
[OwnCapability.SEND_VIDEO]: TrackType.VIDEO,
|
|
12940
|
+
[OwnCapability.SCREENSHARE]: TrackType.SCREEN_SHARE,
|
|
12941
|
+
};
|
|
12942
|
+
for (const [permission, trackType] of Object.entries(permissionToTrackType)) {
|
|
12943
|
+
const hasPermission = this.permissionsContext.hasPermission(permission);
|
|
12944
|
+
if (hasPermission)
|
|
12945
|
+
continue;
|
|
12946
|
+
try {
|
|
12947
|
+
switch (trackType) {
|
|
12948
|
+
case TrackType.AUDIO:
|
|
12949
|
+
if (this.microphone.enabled)
|
|
12950
|
+
await this.microphone.disable();
|
|
12951
|
+
break;
|
|
12952
|
+
case TrackType.VIDEO:
|
|
12953
|
+
if (this.camera.enabled)
|
|
12954
|
+
await this.camera.disable();
|
|
12955
|
+
break;
|
|
12956
|
+
case TrackType.SCREEN_SHARE:
|
|
12957
|
+
if (this.screenShare.enabled)
|
|
12958
|
+
await this.screenShare.disable();
|
|
12959
|
+
break;
|
|
12960
|
+
}
|
|
12961
|
+
}
|
|
12962
|
+
catch (err) {
|
|
12963
|
+
this.logger('error', `Can't disable mic/camera/screenshare after revoked permissions`, err);
|
|
12964
|
+
}
|
|
12965
|
+
}
|
|
12966
|
+
};
|
|
12513
12967
|
/**
|
|
12514
12968
|
* You can subscribe to WebSocket events provided by the API. To remove a subscription, call the `off` method.
|
|
12515
12969
|
* Please note that subscribing to WebSocket events is an advanced use-case.
|
|
@@ -12560,9 +13014,8 @@ class Call {
|
|
|
12560
13014
|
throw new Error('Cannot leave call that has already been left.');
|
|
12561
13015
|
}
|
|
12562
13016
|
if (callingState === exports.CallingState.JOINING) {
|
|
12563
|
-
await this.
|
|
13017
|
+
await this.waitUntilCallJoined();
|
|
12564
13018
|
}
|
|
12565
|
-
this.isLeaving = true;
|
|
12566
13019
|
if (this.ringing) {
|
|
12567
13020
|
// I'm the one who started the call, so I should cancel it.
|
|
12568
13021
|
const hasOtherParticipants = this.state.remoteParticipants.length > 0;
|
|
@@ -12584,14 +13037,15 @@ class Call {
|
|
|
12584
13037
|
this.sfuStatsReporter = undefined;
|
|
12585
13038
|
this.subscriber?.close();
|
|
12586
13039
|
this.subscriber = undefined;
|
|
12587
|
-
this.publisher?.close();
|
|
13040
|
+
this.publisher?.close({ stopTracks: true });
|
|
12588
13041
|
this.publisher = undefined;
|
|
12589
|
-
this.sfuClient?.
|
|
13042
|
+
await this.sfuClient?.leaveAndClose(reason);
|
|
12590
13043
|
this.sfuClient = undefined;
|
|
12591
13044
|
this.state.setCallingState(exports.CallingState.LEFT);
|
|
12592
13045
|
// Call all leave call hooks, e.g. to clean up global event handlers
|
|
12593
13046
|
this.leaveCallHooks.forEach((hook) => hook());
|
|
12594
13047
|
this.initialized = false;
|
|
13048
|
+
this.hasJoinedOnce = false;
|
|
12595
13049
|
this.clientStore.unregisterCall(this);
|
|
12596
13050
|
this.camera.dispose();
|
|
12597
13051
|
this.microphone.dispose();
|
|
@@ -12631,7 +13085,7 @@ class Call {
|
|
|
12631
13085
|
this.watching = true;
|
|
12632
13086
|
this.clientStore.registerCall(this);
|
|
12633
13087
|
}
|
|
12634
|
-
await this.applyDeviceConfig();
|
|
13088
|
+
await this.applyDeviceConfig(false);
|
|
12635
13089
|
return response;
|
|
12636
13090
|
};
|
|
12637
13091
|
/**
|
|
@@ -12653,7 +13107,7 @@ class Call {
|
|
|
12653
13107
|
this.watching = true;
|
|
12654
13108
|
this.clientStore.registerCall(this);
|
|
12655
13109
|
}
|
|
12656
|
-
await this.applyDeviceConfig();
|
|
13110
|
+
await this.applyDeviceConfig(false);
|
|
12657
13111
|
return response;
|
|
12658
13112
|
};
|
|
12659
13113
|
/**
|
|
@@ -12709,253 +13163,174 @@ class Call {
|
|
|
12709
13163
|
await this.setup();
|
|
12710
13164
|
const callingState = this.state.callingState;
|
|
12711
13165
|
if ([exports.CallingState.JOINED, exports.CallingState.JOINING].includes(callingState)) {
|
|
12712
|
-
|
|
12713
|
-
throw new Error(`Illegal State: Already joined.`);
|
|
13166
|
+
throw new Error(`Illegal State: call.join() shall be called only once`);
|
|
12714
13167
|
}
|
|
12715
|
-
|
|
12716
|
-
const isReconnecting = callingState === exports.CallingState.RECONNECTING;
|
|
12717
|
-
this.state.setCallingState(exports.CallingState.JOINING);
|
|
13168
|
+
this.joinCallData = data;
|
|
12718
13169
|
this.logger('debug', 'Starting join flow');
|
|
12719
|
-
|
|
12720
|
-
|
|
12721
|
-
|
|
12722
|
-
|
|
12723
|
-
|
|
12724
|
-
|
|
12725
|
-
|
|
12726
|
-
|
|
12727
|
-
|
|
12728
|
-
|
|
12729
|
-
|
|
12730
|
-
|
|
12731
|
-
|
|
12732
|
-
// use previous SFU configuration and values
|
|
12733
|
-
connectionConfig = this.publisher?.connectionConfiguration;
|
|
12734
|
-
sfuServer = this.sfuClient.sfuServer;
|
|
12735
|
-
sfuToken = this.sfuClient.token;
|
|
12736
|
-
statsOptions = this.sfuStatsReporter?.options;
|
|
12737
|
-
}
|
|
12738
|
-
else {
|
|
12739
|
-
// full join flow - let the Coordinator pick a new SFU for us
|
|
12740
|
-
const call = await join(this.streamClient, this.type, this.id, data);
|
|
12741
|
-
this.state.updateFromCallResponse(call.metadata);
|
|
12742
|
-
this.state.setMembers(call.members);
|
|
12743
|
-
this.state.setOwnCapabilities(call.ownCapabilities);
|
|
12744
|
-
connectionConfig = call.connectionConfig;
|
|
12745
|
-
sfuServer = call.sfuServer;
|
|
12746
|
-
sfuToken = call.token;
|
|
12747
|
-
statsOptions = call.statsOptions;
|
|
13170
|
+
this.state.setCallingState(exports.CallingState.JOINING);
|
|
13171
|
+
const performingMigration = this.reconnectStrategy === WebsocketReconnectStrategy.MIGRATE;
|
|
13172
|
+
const performingRejoin = this.reconnectStrategy === WebsocketReconnectStrategy.REJOIN;
|
|
13173
|
+
const performingFastReconnect = this.reconnectStrategy === WebsocketReconnectStrategy.FAST;
|
|
13174
|
+
let statsOptions = this.sfuStatsReporter?.options;
|
|
13175
|
+
if (!this.credentials ||
|
|
13176
|
+
!statsOptions ||
|
|
13177
|
+
performingRejoin ||
|
|
13178
|
+
performingMigration) {
|
|
13179
|
+
try {
|
|
13180
|
+
const joinResponse = await this.doJoinRequest(data);
|
|
13181
|
+
this.credentials = joinResponse.credentials;
|
|
13182
|
+
statsOptions = joinResponse.stats_options;
|
|
12748
13183
|
}
|
|
12749
|
-
|
|
12750
|
-
|
|
12751
|
-
this.
|
|
13184
|
+
catch (error) {
|
|
13185
|
+
// restore the previous call state if the join-flow fails
|
|
13186
|
+
this.state.setCallingState(callingState);
|
|
13187
|
+
throw error;
|
|
12752
13188
|
}
|
|
12753
13189
|
}
|
|
12754
|
-
catch (error) {
|
|
12755
|
-
// restore the previous call state if the join-flow fails
|
|
12756
|
-
this.state.setCallingState(callingState);
|
|
12757
|
-
throw error;
|
|
12758
|
-
}
|
|
12759
13190
|
const previousSfuClient = this.sfuClient;
|
|
12760
|
-
const
|
|
12761
|
-
|
|
12762
|
-
|
|
12763
|
-
|
|
12764
|
-
|
|
12765
|
-
|
|
12766
|
-
|
|
12767
|
-
|
|
12768
|
-
|
|
12769
|
-
|
|
12770
|
-
|
|
12771
|
-
|
|
12772
|
-
|
|
12773
|
-
|
|
12774
|
-
|
|
12775
|
-
|
|
12776
|
-
|
|
12777
|
-
|
|
12778
|
-
|
|
12779
|
-
|
|
12780
|
-
|
|
12781
|
-
|
|
12782
|
-
|
|
12783
|
-
|
|
12784
|
-
|
|
12785
|
-
|
|
12786
|
-
|
|
12787
|
-
|
|
12788
|
-
|
|
12789
|
-
const localParticipant = this.state.localParticipant;
|
|
12790
|
-
if (strategy === 'fast') {
|
|
12791
|
-
sfuClient.close(StreamSfuClient.ERROR_CONNECTION_BROKEN, `attempting fast reconnect: ${reason}`);
|
|
12792
|
-
}
|
|
12793
|
-
else if (strategy === 'full') {
|
|
12794
|
-
// in migration or recovery scenarios, we don't want to
|
|
12795
|
-
// wait before attempting to reconnect to an SFU server
|
|
12796
|
-
await sleep(retryInterval(this.reconnectAttempts));
|
|
12797
|
-
// in full-reconnect, we need to dispose all Peer Connections
|
|
12798
|
-
this.subscriber?.close();
|
|
12799
|
-
this.subscriber = undefined;
|
|
12800
|
-
this.publisher?.close({ stopTracks: false });
|
|
12801
|
-
this.publisher = undefined;
|
|
12802
|
-
this.statsReporter?.stop();
|
|
12803
|
-
this.statsReporter = undefined;
|
|
12804
|
-
this.sfuStatsReporter?.stop();
|
|
12805
|
-
this.sfuStatsReporter = undefined;
|
|
12806
|
-
// clean up current connection
|
|
12807
|
-
sfuClient.close(StreamSfuClient.NORMAL_CLOSURE, `attempting full reconnect: ${reason}`);
|
|
12808
|
-
}
|
|
12809
|
-
await this.join({
|
|
12810
|
-
...data,
|
|
12811
|
-
...(strategy === 'migrate' && { migrating_from: sfuServer.edge_name }),
|
|
13191
|
+
const previousSessionId = previousSfuClient?.sessionId;
|
|
13192
|
+
const isWsHealthy = !!previousSfuClient?.isHealthy;
|
|
13193
|
+
const sfuClient = performingRejoin || performingMigration || !isWsHealthy
|
|
13194
|
+
? new StreamSfuClient({
|
|
13195
|
+
logTag: String(this.sfuClientTag++),
|
|
13196
|
+
dispatcher: this.dispatcher,
|
|
13197
|
+
credentials: this.credentials,
|
|
13198
|
+
// a new session_id is necessary for the REJOIN strategy.
|
|
13199
|
+
// we use the previous session_id if available
|
|
13200
|
+
sessionId: performingRejoin ? undefined : previousSessionId,
|
|
13201
|
+
onSignalClose: () => this.handleSfuSignalClose(sfuClient),
|
|
13202
|
+
})
|
|
13203
|
+
: previousSfuClient;
|
|
13204
|
+
this.sfuClient = sfuClient;
|
|
13205
|
+
const clientDetails = getClientDetails();
|
|
13206
|
+
// we don't need to send JoinRequest if we are re-using an existing healthy SFU client
|
|
13207
|
+
if (previousSfuClient !== sfuClient) {
|
|
13208
|
+
// prepare a generic SDP and send it to the SFU.
|
|
13209
|
+
// this is a throw-away SDP that the SFU will use to determine
|
|
13210
|
+
// the capabilities of the client (codec support, etc.)
|
|
13211
|
+
const receivingCapabilitiesSdp = await getGenericSdp('recvonly');
|
|
13212
|
+
const reconnectDetails = this.reconnectStrategy !== WebsocketReconnectStrategy.UNSPECIFIED
|
|
13213
|
+
? this.getReconnectDetails(data?.migrating_from, previousSessionId)
|
|
13214
|
+
: undefined;
|
|
13215
|
+
const { callState, fastReconnectDeadlineSeconds } = await sfuClient.join({
|
|
13216
|
+
subscriberSdp: receivingCapabilitiesSdp,
|
|
13217
|
+
clientDetails,
|
|
13218
|
+
fastReconnect: performingFastReconnect,
|
|
13219
|
+
reconnectDetails,
|
|
12812
13220
|
});
|
|
12813
|
-
|
|
12814
|
-
if (
|
|
12815
|
-
|
|
12816
|
-
}
|
|
12817
|
-
this.logger('info', `[Rejoin]: Attempt ${this.reconnectAttempts} successful!`);
|
|
12818
|
-
// we shouldn't be republishing the streams if we're migrating
|
|
12819
|
-
// as the underlying peer connection will take care of it as part
|
|
12820
|
-
// of the ice-restart process
|
|
12821
|
-
if (localParticipant && strategy === 'full') {
|
|
12822
|
-
const { audioStream, videoStream, screenShareStream, screenShareAudioStream, } = localParticipant;
|
|
12823
|
-
let screenShare;
|
|
12824
|
-
if (screenShareStream || screenShareAudioStream) {
|
|
12825
|
-
screenShare = new MediaStream();
|
|
12826
|
-
screenShareStream?.getVideoTracks().forEach((track) => {
|
|
12827
|
-
screenShare?.addTrack(track);
|
|
12828
|
-
});
|
|
12829
|
-
screenShareAudioStream?.getAudioTracks().forEach((track) => {
|
|
12830
|
-
screenShare?.addTrack(track);
|
|
12831
|
-
});
|
|
12832
|
-
}
|
|
12833
|
-
// restore previous publishing state
|
|
12834
|
-
if (audioStream)
|
|
12835
|
-
await this.publishAudioStream(audioStream);
|
|
12836
|
-
if (videoStream) {
|
|
12837
|
-
await this.publishVideoStream(videoStream, {
|
|
12838
|
-
preferredCodec: this.camera.preferredCodec,
|
|
12839
|
-
});
|
|
12840
|
-
}
|
|
12841
|
-
if (screenShare)
|
|
12842
|
-
await this.publishScreenShareStream(screenShare);
|
|
12843
|
-
this.logger('info', `[Rejoin]: State restored. Attempt: ${this.reconnectAttempts}`);
|
|
13221
|
+
this.fastReconnectDeadlineSeconds = fastReconnectDeadlineSeconds;
|
|
13222
|
+
if (callState) {
|
|
13223
|
+
this.state.updateFromSfuCallState(callState, sfuClient.sessionId, reconnectDetails);
|
|
12844
13224
|
}
|
|
12845
|
-
}
|
|
12846
|
-
|
|
12847
|
-
|
|
12848
|
-
|
|
12849
|
-
|
|
12850
|
-
|
|
12851
|
-
|
|
12852
|
-
|
|
12853
|
-
|
|
12854
|
-
|
|
12855
|
-
|
|
12856
|
-
|
|
12857
|
-
|
|
12858
|
-
|
|
12859
|
-
|
|
12860
|
-
|
|
12861
|
-
unregisterGoAway();
|
|
12862
|
-
// when the user has initiated "call.leave()" operation, we shouldn't
|
|
12863
|
-
// care for the WS close code and we shouldn't ever attempt to reconnect
|
|
12864
|
-
if (this.isLeaving)
|
|
12865
|
-
return;
|
|
12866
|
-
// do nothing if the connection was closed on purpose
|
|
12867
|
-
if (e.code === StreamSfuClient.NORMAL_CLOSURE)
|
|
12868
|
-
return;
|
|
12869
|
-
// do nothing if the connection was closed because of a policy violation
|
|
12870
|
-
// e.g., the user has been blocked by an admin or moderator
|
|
12871
|
-
if (e.code === KnownCodes.WS_POLICY_VIOLATION)
|
|
12872
|
-
return;
|
|
12873
|
-
// When the SFU is being shut down, it sends a goAway message.
|
|
12874
|
-
// While we migrate to another SFU, we might have the WS connection
|
|
12875
|
-
// to the old SFU closed abruptly. In this case, we don't want
|
|
12876
|
-
// to reconnect to the old SFU, but rather to the new one.
|
|
12877
|
-
const isMigratingAway = e.code === KnownCodes.WS_CLOSED_ABRUPTLY && sfuClient.isMigratingAway;
|
|
12878
|
-
const isFastReconnecting = e.code === KnownCodes.WS_CLOSED_ABRUPTLY &&
|
|
12879
|
-
sfuClient.isFastReconnecting;
|
|
12880
|
-
if (isMigratingAway || isFastReconnecting)
|
|
12881
|
-
return;
|
|
12882
|
-
// do nothing if the connection was closed because of a fast reconnect
|
|
12883
|
-
if (e.code === StreamSfuClient.ERROR_CONNECTION_BROKEN)
|
|
12884
|
-
return;
|
|
12885
|
-
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
12886
|
-
sfuClient.isFastReconnecting = this.reconnectAttempts === 0;
|
|
12887
|
-
const strategy = sfuClient.isFastReconnecting ? 'fast' : 'full';
|
|
12888
|
-
reconnect(strategy, `SFU closed the WS with code: ${e.code}`).catch((err) => {
|
|
12889
|
-
this.logger('error', `[Rejoin]: ${strategy} rejoin failed for ${this.reconnectAttempts} times. Giving up.`, err);
|
|
12890
|
-
this.state.setCallingState(exports.CallingState.RECONNECTING_FAILED);
|
|
12891
|
-
});
|
|
12892
|
-
}
|
|
12893
|
-
else {
|
|
12894
|
-
this.logger('error', '[Rejoin]: Reconnect attempts exceeded. Giving up...');
|
|
12895
|
-
this.state.setCallingState(exports.CallingState.RECONNECTING_FAILED);
|
|
12896
|
-
}
|
|
12897
|
-
});
|
|
12898
|
-
});
|
|
12899
|
-
// handlers for connection online/offline events
|
|
12900
|
-
const unsubscribeOnlineEvent = this.streamClient.on('connection.changed', async (e) => {
|
|
12901
|
-
if (e.type !== 'connection.changed')
|
|
12902
|
-
return;
|
|
12903
|
-
if (!e.online)
|
|
12904
|
-
return;
|
|
12905
|
-
unsubscribeOnlineEvent();
|
|
12906
|
-
const currentCallingState = this.state.callingState;
|
|
12907
|
-
const shouldReconnect = currentCallingState === exports.CallingState.OFFLINE ||
|
|
12908
|
-
currentCallingState === exports.CallingState.RECONNECTING_FAILED;
|
|
12909
|
-
if (!shouldReconnect)
|
|
12910
|
-
return;
|
|
12911
|
-
this.logger('info', '[Rejoin]: Going online...');
|
|
12912
|
-
let isFirstReconnectAttempt = true;
|
|
12913
|
-
do {
|
|
12914
|
-
try {
|
|
12915
|
-
sfuClient.isFastReconnecting = isFirstReconnectAttempt;
|
|
12916
|
-
await reconnect(isFirstReconnectAttempt ? 'fast' : 'full', 'Network: online');
|
|
12917
|
-
return; // break the loop if rejoin is successful
|
|
12918
|
-
}
|
|
12919
|
-
catch (err) {
|
|
12920
|
-
this.logger('error', `[Rejoin][Network]: Rejoin failed for attempt ${this.reconnectAttempts}`, err);
|
|
12921
|
-
}
|
|
12922
|
-
// wait for a bit before trying to reconnect again
|
|
12923
|
-
await sleep(retryInterval(this.reconnectAttempts));
|
|
12924
|
-
isFirstReconnectAttempt = false;
|
|
12925
|
-
} while (this.reconnectAttempts < this.maxReconnectAttempts);
|
|
12926
|
-
// if we're here, it means that we've exhausted all the reconnect attempts
|
|
12927
|
-
this.logger('error', `[Rejoin][Network]: Rejoin failed. Giving up.`);
|
|
12928
|
-
this.state.setCallingState(exports.CallingState.RECONNECTING_FAILED);
|
|
12929
|
-
});
|
|
12930
|
-
const unsubscribeOfflineEvent = this.streamClient.on('connection.changed', (e) => {
|
|
12931
|
-
if (e.type !== 'connection.changed')
|
|
12932
|
-
return;
|
|
12933
|
-
if (e.online)
|
|
12934
|
-
return;
|
|
12935
|
-
unsubscribeOfflineEvent();
|
|
12936
|
-
this.state.setCallingState(exports.CallingState.OFFLINE);
|
|
12937
|
-
});
|
|
12938
|
-
this.leaveCallHooks.add(() => {
|
|
12939
|
-
unsubscribeOnlineEvent();
|
|
12940
|
-
unsubscribeOfflineEvent();
|
|
12941
|
-
});
|
|
12942
|
-
if (!this.subscriber) {
|
|
12943
|
-
this.subscriber = new Subscriber({
|
|
13225
|
+
}
|
|
13226
|
+
if (!performingMigration) {
|
|
13227
|
+
// in MIGRATION, `JOINED` state is set in `this.reconnectMigrate()`
|
|
13228
|
+
this.state.setCallingState(exports.CallingState.JOINED);
|
|
13229
|
+
}
|
|
13230
|
+
this.hasJoinedOnce = true;
|
|
13231
|
+
// when performing fast reconnect, or when we reuse the same SFU client,
|
|
13232
|
+
// (ws remained healthy), we just need to restore the ICE connection
|
|
13233
|
+
if (performingFastReconnect) {
|
|
13234
|
+
// the SFU automatically issues an ICE restart on the subscriber
|
|
13235
|
+
// we don't have to do it ourselves
|
|
13236
|
+
await this.restoreICE(sfuClient, { includeSubscriber: false });
|
|
13237
|
+
}
|
|
13238
|
+
else {
|
|
13239
|
+
const connectionConfig = toRtcConfiguration(this.credentials.ice_servers);
|
|
13240
|
+
this.initPublisherAndSubscriber({
|
|
12944
13241
|
sfuClient,
|
|
12945
|
-
dispatcher: this.dispatcher,
|
|
12946
|
-
state: this.state,
|
|
12947
13242
|
connectionConfig,
|
|
12948
|
-
|
|
12949
|
-
|
|
12950
|
-
|
|
12951
|
-
});
|
|
12952
|
-
},
|
|
13243
|
+
clientDetails,
|
|
13244
|
+
statsOptions,
|
|
13245
|
+
closePreviousInstances: !performingMigration,
|
|
12953
13246
|
});
|
|
12954
13247
|
}
|
|
13248
|
+
if (performingRejoin) {
|
|
13249
|
+
const strategy = WebsocketReconnectStrategy[this.reconnectStrategy];
|
|
13250
|
+
await previousSfuClient?.leaveAndClose(`Closing previous WS after reconnect with strategy: ${strategy}`);
|
|
13251
|
+
}
|
|
13252
|
+
else if (!isWsHealthy) {
|
|
13253
|
+
previousSfuClient?.close(4002, 'Closing unhealthy WS after reconnect');
|
|
13254
|
+
}
|
|
13255
|
+
// device settings should be applied only once, we don't have to
|
|
13256
|
+
// re-apply them on later reconnections or server-side data fetches
|
|
13257
|
+
if (!this.deviceSettingsAppliedOnce) {
|
|
13258
|
+
await this.applyDeviceConfig(true);
|
|
13259
|
+
this.deviceSettingsAppliedOnce = true;
|
|
13260
|
+
}
|
|
13261
|
+
this.logger('info', `Joined call ${this.cid}`);
|
|
13262
|
+
};
|
|
13263
|
+
/**
|
|
13264
|
+
* Prepares Reconnect Details object.
|
|
13265
|
+
* @internal
|
|
13266
|
+
*/
|
|
13267
|
+
this.getReconnectDetails = (migratingFromSfuId, previousSessionId) => {
|
|
13268
|
+
const strategy = this.reconnectStrategy;
|
|
13269
|
+
const performingRejoin = strategy === WebsocketReconnectStrategy.REJOIN;
|
|
13270
|
+
const announcedTracks = this.publisher?.getAnnouncedTracks() || [];
|
|
13271
|
+
const subscribedTracks = getCurrentValue(this.trackSubscriptionsSubject);
|
|
13272
|
+
return {
|
|
13273
|
+
strategy,
|
|
13274
|
+
announcedTracks,
|
|
13275
|
+
subscriptions: subscribedTracks.data || [],
|
|
13276
|
+
reconnectAttempt: this.reconnectAttempts,
|
|
13277
|
+
fromSfuId: migratingFromSfuId || '',
|
|
13278
|
+
previousSessionId: performingRejoin ? previousSessionId || '' : '',
|
|
13279
|
+
};
|
|
13280
|
+
};
|
|
13281
|
+
/**
|
|
13282
|
+
* Performs an ICE restart on both the Publisher and Subscriber Peer Connections.
|
|
13283
|
+
* Uses the provided SFU client to restore the ICE connection.
|
|
13284
|
+
*
|
|
13285
|
+
* This method can throw an error if the ICE restart fails.
|
|
13286
|
+
* This error should be handled by the reconnect loop,
|
|
13287
|
+
* and a new reconnection shall be attempted.
|
|
13288
|
+
*
|
|
13289
|
+
* @internal
|
|
13290
|
+
*/
|
|
13291
|
+
this.restoreICE = async (nextSfuClient, opts = {}) => {
|
|
13292
|
+
const { includeSubscriber = true, includePublisher = true } = opts;
|
|
13293
|
+
if (this.subscriber) {
|
|
13294
|
+
this.subscriber.setSfuClient(nextSfuClient);
|
|
13295
|
+
if (includeSubscriber) {
|
|
13296
|
+
await this.subscriber.restartIce();
|
|
13297
|
+
}
|
|
13298
|
+
}
|
|
13299
|
+
if (this.publisher) {
|
|
13300
|
+
this.publisher.setSfuClient(nextSfuClient);
|
|
13301
|
+
if (includePublisher) {
|
|
13302
|
+
await this.publisher.restartIce();
|
|
13303
|
+
}
|
|
13304
|
+
}
|
|
13305
|
+
};
|
|
13306
|
+
/**
|
|
13307
|
+
* Initializes the Publisher and Subscriber Peer Connections.
|
|
13308
|
+
* @internal
|
|
13309
|
+
*/
|
|
13310
|
+
this.initPublisherAndSubscriber = (opts) => {
|
|
13311
|
+
const { sfuClient, connectionConfig, clientDetails, statsOptions, closePreviousInstances, } = opts;
|
|
13312
|
+
if (closePreviousInstances && this.subscriber) {
|
|
13313
|
+
this.subscriber.close();
|
|
13314
|
+
}
|
|
13315
|
+
this.subscriber = new Subscriber({
|
|
13316
|
+
sfuClient,
|
|
13317
|
+
dispatcher: this.dispatcher,
|
|
13318
|
+
state: this.state,
|
|
13319
|
+
connectionConfig,
|
|
13320
|
+
logTag: String(this.reconnectAttempts),
|
|
13321
|
+
onUnrecoverableError: () => {
|
|
13322
|
+
this.reconnect(WebsocketReconnectStrategy.REJOIN).catch((err) => {
|
|
13323
|
+
this.logger('warn', '[Reconnect] Error reconnecting after a subscriber error', err);
|
|
13324
|
+
});
|
|
13325
|
+
},
|
|
13326
|
+
});
|
|
12955
13327
|
// anonymous users can't publish anything hence, there is no need
|
|
12956
13328
|
// to create Publisher Peer Connection for them
|
|
12957
13329
|
const isAnonymous = this.streamClient.user?.type === 'anonymous';
|
|
12958
|
-
if (!
|
|
13330
|
+
if (!isAnonymous) {
|
|
13331
|
+
if (closePreviousInstances && this.publisher) {
|
|
13332
|
+
this.publisher.close({ stopTracks: false });
|
|
13333
|
+
}
|
|
12959
13334
|
const audioSettings = this.state.settings?.audio;
|
|
12960
13335
|
const isDtxEnabled = !!audioSettings?.opus_dtx_enabled;
|
|
12961
13336
|
const isRedEnabled = !!audioSettings?.redundant_coding_enabled;
|
|
@@ -12966,23 +13341,23 @@ class Call {
|
|
|
12966
13341
|
connectionConfig,
|
|
12967
13342
|
isDtxEnabled,
|
|
12968
13343
|
isRedEnabled,
|
|
13344
|
+
logTag: String(this.reconnectAttempts),
|
|
12969
13345
|
onUnrecoverableError: () => {
|
|
12970
|
-
reconnect(
|
|
12971
|
-
this.logger('
|
|
13346
|
+
this.reconnect(WebsocketReconnectStrategy.REJOIN).catch((err) => {
|
|
13347
|
+
this.logger('warn', '[Reconnect] Error reconnecting after a publisher error', err);
|
|
12972
13348
|
});
|
|
12973
13349
|
},
|
|
12974
13350
|
});
|
|
12975
13351
|
}
|
|
12976
|
-
|
|
12977
|
-
|
|
12978
|
-
|
|
12979
|
-
|
|
12980
|
-
|
|
12981
|
-
|
|
12982
|
-
|
|
12983
|
-
|
|
12984
|
-
|
|
12985
|
-
if (!this.sfuStatsReporter && statsOptions) {
|
|
13352
|
+
this.statsReporter?.stop();
|
|
13353
|
+
this.statsReporter = createStatsReporter({
|
|
13354
|
+
subscriber: this.subscriber,
|
|
13355
|
+
publisher: this.publisher,
|
|
13356
|
+
state: this.state,
|
|
13357
|
+
datacenter: sfuClient.edgeName,
|
|
13358
|
+
});
|
|
13359
|
+
this.sfuStatsReporter?.stop();
|
|
13360
|
+
if (statsOptions?.reporting_interval_ms > 0) {
|
|
12986
13361
|
this.sfuStatsReporter = new SfuStatsReporter(sfuClient, {
|
|
12987
13362
|
clientDetails,
|
|
12988
13363
|
options: statsOptions,
|
|
@@ -12991,129 +13366,262 @@ class Call {
|
|
|
12991
13366
|
});
|
|
12992
13367
|
this.sfuStatsReporter.start();
|
|
12993
13368
|
}
|
|
12994
|
-
|
|
12995
|
-
|
|
12996
|
-
|
|
12997
|
-
|
|
12998
|
-
|
|
12999
|
-
|
|
13000
|
-
|
|
13001
|
-
|
|
13002
|
-
|
|
13003
|
-
|
|
13004
|
-
|
|
13005
|
-
|
|
13006
|
-
|
|
13007
|
-
|
|
13008
|
-
|
|
13369
|
+
};
|
|
13370
|
+
/**
|
|
13371
|
+
* Retrieves credentials for joining the call.
|
|
13372
|
+
*
|
|
13373
|
+
* @internal
|
|
13374
|
+
*
|
|
13375
|
+
* @param data the join call data.
|
|
13376
|
+
*/
|
|
13377
|
+
this.doJoinRequest = async (data) => {
|
|
13378
|
+
const location = await this.streamClient.getLocationHint();
|
|
13379
|
+
const request = { ...data, location };
|
|
13380
|
+
const joinResponse = await this.streamClient.post(`${this.streamClientBasePath}/join`, request);
|
|
13381
|
+
this.state.updateFromCallResponse(joinResponse.call);
|
|
13382
|
+
this.state.setMembers(joinResponse.members);
|
|
13383
|
+
this.state.setOwnCapabilities(joinResponse.own_capabilities);
|
|
13384
|
+
if (data?.ring && !this.ringing) {
|
|
13385
|
+
this.ringingSubject.next(true);
|
|
13386
|
+
}
|
|
13387
|
+
if (this.ringing && !this.isCreatedByMe) {
|
|
13388
|
+
// signals other users that I have accepted the incoming call.
|
|
13389
|
+
await this.accept();
|
|
13390
|
+
}
|
|
13391
|
+
if (this.streamClient._hasConnectionID()) {
|
|
13392
|
+
this.watching = true;
|
|
13393
|
+
this.clientStore.registerCall(this);
|
|
13394
|
+
}
|
|
13395
|
+
return joinResponse;
|
|
13396
|
+
};
|
|
13397
|
+
/**
|
|
13398
|
+
* Handles the reconnection flow.
|
|
13399
|
+
*
|
|
13400
|
+
* @internal
|
|
13401
|
+
*
|
|
13402
|
+
* @param strategy the reconnection strategy to use.
|
|
13403
|
+
*/
|
|
13404
|
+
this.reconnect = async (strategy) => {
|
|
13405
|
+
return withoutConcurrency(this.reconnectConcurrencyTag, async () => {
|
|
13406
|
+
this.logger('info', `[Reconnect] Reconnecting with strategy ${WebsocketReconnectStrategy[strategy]}`);
|
|
13407
|
+
this.reconnectStrategy = strategy;
|
|
13408
|
+
do {
|
|
13409
|
+
this.reconnectAttempts++;
|
|
13410
|
+
const current = WebsocketReconnectStrategy[this.reconnectStrategy];
|
|
13411
|
+
try {
|
|
13412
|
+
// wait until the network is available
|
|
13413
|
+
await this.networkAvailableTask?.promise;
|
|
13414
|
+
switch (this.reconnectStrategy) {
|
|
13415
|
+
case WebsocketReconnectStrategy.UNSPECIFIED:
|
|
13416
|
+
case WebsocketReconnectStrategy.DISCONNECT:
|
|
13417
|
+
this.logger('debug', `[Reconnect] No-op strategy ${current}`);
|
|
13418
|
+
break;
|
|
13419
|
+
case WebsocketReconnectStrategy.FAST:
|
|
13420
|
+
await this.reconnectFast();
|
|
13421
|
+
break;
|
|
13422
|
+
case WebsocketReconnectStrategy.REJOIN:
|
|
13423
|
+
await this.reconnectRejoin();
|
|
13424
|
+
break;
|
|
13425
|
+
case WebsocketReconnectStrategy.MIGRATE:
|
|
13426
|
+
await this.reconnectMigrate();
|
|
13427
|
+
break;
|
|
13428
|
+
default:
|
|
13429
|
+
ensureExhausted(this.reconnectStrategy, 'Unknown reconnection strategy');
|
|
13430
|
+
break;
|
|
13009
13431
|
}
|
|
13010
|
-
|
|
13011
|
-
|
|
13012
|
-
|
|
13013
|
-
|
|
13014
|
-
|
|
13015
|
-
|
|
13016
|
-
}
|
|
13432
|
+
break; // do-while loop, reconnection worked, exit the loop
|
|
13433
|
+
}
|
|
13434
|
+
catch (error) {
|
|
13435
|
+
this.logger('warn', `[Reconnect] ${current}(${this.reconnectAttempts}) failed. Attempting with REJOIN`, error);
|
|
13436
|
+
await sleep(500);
|
|
13437
|
+
this.reconnectStrategy = WebsocketReconnectStrategy.REJOIN;
|
|
13438
|
+
}
|
|
13439
|
+
} while (this.state.callingState !== exports.CallingState.JOINED &&
|
|
13440
|
+
this.state.callingState !== exports.CallingState.LEFT);
|
|
13441
|
+
});
|
|
13442
|
+
};
|
|
13443
|
+
/**
|
|
13444
|
+
* Initiates the reconnection flow with the "fast" strategy.
|
|
13445
|
+
* @internal
|
|
13446
|
+
*/
|
|
13447
|
+
this.reconnectFast = async () => {
|
|
13448
|
+
this.reconnectStrategy = WebsocketReconnectStrategy.FAST;
|
|
13449
|
+
this.state.setCallingState(exports.CallingState.RECONNECTING);
|
|
13450
|
+
return this.join(this.joinCallData);
|
|
13451
|
+
};
|
|
13452
|
+
/**
|
|
13453
|
+
* Initiates the reconnection flow with the "rejoin" strategy.
|
|
13454
|
+
* @internal
|
|
13455
|
+
*/
|
|
13456
|
+
this.reconnectRejoin = async () => {
|
|
13457
|
+
this.reconnectStrategy = WebsocketReconnectStrategy.REJOIN;
|
|
13458
|
+
this.state.setCallingState(exports.CallingState.RECONNECTING);
|
|
13459
|
+
await this.join(this.joinCallData);
|
|
13460
|
+
await this.restorePublishedTracks();
|
|
13461
|
+
this.restoreSubscribedTracks();
|
|
13462
|
+
};
|
|
13463
|
+
/**
|
|
13464
|
+
* Initiates the reconnection flow with the "migrate" strategy.
|
|
13465
|
+
* @internal
|
|
13466
|
+
*/
|
|
13467
|
+
this.reconnectMigrate = async () => {
|
|
13468
|
+
const currentSfuClient = this.sfuClient;
|
|
13469
|
+
if (!currentSfuClient) {
|
|
13470
|
+
throw new Error('Cannot migrate without an active SFU client');
|
|
13471
|
+
}
|
|
13472
|
+
this.reconnectStrategy = WebsocketReconnectStrategy.MIGRATE;
|
|
13473
|
+
this.state.setCallingState(exports.CallingState.MIGRATING);
|
|
13474
|
+
const currentSubscriber = this.subscriber;
|
|
13475
|
+
const currentPublisher = this.publisher;
|
|
13476
|
+
currentSubscriber?.detachEventHandlers();
|
|
13477
|
+
currentPublisher?.detachEventHandlers();
|
|
13478
|
+
const migrationTask = currentSfuClient.enterMigration();
|
|
13479
|
+
try {
|
|
13480
|
+
const currentSfu = currentSfuClient.edgeName;
|
|
13481
|
+
await this.join({ ...this.joinCallData, migrating_from: currentSfu });
|
|
13482
|
+
}
|
|
13483
|
+
finally {
|
|
13484
|
+
// cleanup the migration_from field after the migration is complete or failed
|
|
13485
|
+
// as we don't want to keep dirty data in the join call data
|
|
13486
|
+
delete this.joinCallData?.migrating_from;
|
|
13487
|
+
}
|
|
13488
|
+
await this.restorePublishedTracks();
|
|
13489
|
+
this.restoreSubscribedTracks();
|
|
13490
|
+
try {
|
|
13491
|
+
// Wait for the migration to complete, then close the previous SFU client
|
|
13492
|
+
// and the peer connection instances. In case of failure, the migration
|
|
13493
|
+
// task would throw an error and REJOIN would be attempted.
|
|
13494
|
+
await migrationTask;
|
|
13495
|
+
// in MIGRATE, we can consider the call as joined only after
|
|
13496
|
+
// `participantMigrationComplete` event is received, signaled by
|
|
13497
|
+
// the `migrationTask`
|
|
13498
|
+
this.state.setCallingState(exports.CallingState.JOINED);
|
|
13499
|
+
}
|
|
13500
|
+
finally {
|
|
13501
|
+
currentSubscriber?.close();
|
|
13502
|
+
currentPublisher?.close({ stopTracks: false });
|
|
13503
|
+
// and close the previous SFU client, without specifying close code
|
|
13504
|
+
currentSfuClient.close();
|
|
13505
|
+
}
|
|
13506
|
+
};
|
|
13507
|
+
/**
|
|
13508
|
+
* Registers the various event handlers for reconnection.
|
|
13509
|
+
*
|
|
13510
|
+
* @internal
|
|
13511
|
+
*/
|
|
13512
|
+
this.registerReconnectHandlers = () => {
|
|
13513
|
+
// handles the legacy "goAway" event
|
|
13514
|
+
const unregisterGoAway = this.on('goAway', () => {
|
|
13515
|
+
this.reconnect(WebsocketReconnectStrategy.MIGRATE).catch((err) => {
|
|
13516
|
+
this.logger('warn', '[Reconnect] Error reconnecting', err);
|
|
13017
13517
|
});
|
|
13018
|
-
|
|
13019
|
-
|
|
13020
|
-
|
|
13021
|
-
const {
|
|
13022
|
-
if (
|
|
13023
|
-
|
|
13518
|
+
});
|
|
13519
|
+
// handles the "error" event, through which the SFU can request a reconnect
|
|
13520
|
+
const unregisterOnError = this.on('error', (e) => {
|
|
13521
|
+
const { reconnectStrategy: strategy } = e;
|
|
13522
|
+
if (strategy === WebsocketReconnectStrategy.UNSPECIFIED)
|
|
13523
|
+
return;
|
|
13524
|
+
if (strategy === WebsocketReconnectStrategy.DISCONNECT) {
|
|
13525
|
+
this.leave({ reason: 'SFU instructed to disconnect' }).catch((err) => {
|
|
13526
|
+
this.logger('warn', `Can't leave call after disconnect request`, err);
|
|
13527
|
+
});
|
|
13024
13528
|
}
|
|
13025
|
-
|
|
13026
|
-
|
|
13027
|
-
|
|
13529
|
+
else {
|
|
13530
|
+
this.reconnect(strategy).catch((err) => {
|
|
13531
|
+
this.logger('warn', '[Reconnect] Error reconnecting', err);
|
|
13532
|
+
});
|
|
13028
13533
|
}
|
|
13029
|
-
|
|
13030
|
-
|
|
13031
|
-
|
|
13032
|
-
|
|
13033
|
-
|
|
13034
|
-
|
|
13035
|
-
|
|
13036
|
-
|
|
13037
|
-
|
|
13534
|
+
});
|
|
13535
|
+
const unregisterNetworkChanged = this.streamClient.on('network.changed', (e) => {
|
|
13536
|
+
if (!e.online) {
|
|
13537
|
+
this.logger('debug', '[Reconnect] Going offline');
|
|
13538
|
+
if (!this.hasJoinedOnce)
|
|
13539
|
+
return;
|
|
13540
|
+
this.lastOfflineTimestamp = Date.now();
|
|
13541
|
+
// create a new task that would resolve when the network is available
|
|
13542
|
+
const networkAvailableTask = promiseWithResolvers();
|
|
13543
|
+
networkAvailableTask.promise.then(() => {
|
|
13544
|
+
let strategy = WebsocketReconnectStrategy.FAST;
|
|
13545
|
+
if (this.lastOfflineTimestamp) {
|
|
13546
|
+
const offline = (Date.now() - this.lastOfflineTimestamp) / 1000;
|
|
13547
|
+
if (offline > this.fastReconnectDeadlineSeconds) {
|
|
13548
|
+
// We shouldn't attempt FAST if we have exceeded the deadline.
|
|
13549
|
+
// The SFU would have already wiped out the session.
|
|
13550
|
+
strategy = WebsocketReconnectStrategy.REJOIN;
|
|
13551
|
+
}
|
|
13038
13552
|
}
|
|
13039
|
-
|
|
13040
|
-
|
|
13041
|
-
// reconnection wasn't possible, so we need to do a full rejoin
|
|
13042
|
-
return await reconnect('full', 're-attempting').catch((err) => {
|
|
13043
|
-
this.logger('error', `[Rejoin]: Rejoin failed forced full rejoin.`, err);
|
|
13044
|
-
});
|
|
13045
|
-
}
|
|
13046
|
-
}
|
|
13047
|
-
const currentParticipants = callState?.participants || [];
|
|
13048
|
-
const participantCount = callState?.participantCount;
|
|
13049
|
-
const startedAt = callState?.startedAt
|
|
13050
|
-
? Timestamp.toDate(callState.startedAt)
|
|
13051
|
-
: new Date();
|
|
13052
|
-
const pins = callState?.pins ?? [];
|
|
13053
|
-
this.state.setParticipants(() => {
|
|
13054
|
-
const participantLookup = this.state.getParticipantLookupBySessionId();
|
|
13055
|
-
return currentParticipants.map((p) => {
|
|
13056
|
-
// We need to preserve the local state of the participant
|
|
13057
|
-
// (e.g. videoDimension, visibilityState, pinnedAt, etc.)
|
|
13058
|
-
// as it doesn't exist on the server.
|
|
13059
|
-
const existingParticipant = participantLookup[p.sessionId];
|
|
13060
|
-
return Object.assign(p, existingParticipant, {
|
|
13061
|
-
isLocalParticipant: p.sessionId === sfuClient.sessionId,
|
|
13062
|
-
viewportVisibilityState: existingParticipant?.viewportVisibilityState ?? {
|
|
13063
|
-
videoTrack: exports.VisibilityState.UNKNOWN,
|
|
13064
|
-
screenShareTrack: exports.VisibilityState.UNKNOWN,
|
|
13065
|
-
},
|
|
13553
|
+
this.reconnect(strategy).catch((err) => {
|
|
13554
|
+
this.logger('warn', '[Reconnect] Error restoring connection after going online', err);
|
|
13066
13555
|
});
|
|
13067
13556
|
});
|
|
13068
|
-
|
|
13069
|
-
|
|
13070
|
-
|
|
13071
|
-
this.state.setStartedAt(startedAt);
|
|
13072
|
-
this.state.setServerSidePins(pins);
|
|
13073
|
-
this.reconnectAttempts = 0; // reset the reconnect attempts counter
|
|
13074
|
-
this.state.setCallingState(exports.CallingState.JOINED);
|
|
13075
|
-
try {
|
|
13076
|
-
await this.initCamera({ setStatus: true });
|
|
13077
|
-
await this.initMic({ setStatus: true });
|
|
13078
|
-
}
|
|
13079
|
-
catch (error) {
|
|
13080
|
-
this.logger('warn', 'Camera and/or mic init failed during join call', error);
|
|
13081
|
-
}
|
|
13082
|
-
// 3. once we have the "joinResponse", and possibly reconciled the local state
|
|
13083
|
-
// we schedule a fast subscription update for all remote participants
|
|
13084
|
-
// that were visible before we reconnected or migrated to a new SFU.
|
|
13085
|
-
const { remoteParticipants } = this.state;
|
|
13086
|
-
if (remoteParticipants.length > 0) {
|
|
13087
|
-
this.updateSubscriptions(remoteParticipants, exports.DebounceType.FAST);
|
|
13088
|
-
}
|
|
13089
|
-
this.logger('info', `Joined call ${this.cid}`);
|
|
13090
|
-
}
|
|
13091
|
-
catch (err) {
|
|
13092
|
-
// join failed, try to rejoin
|
|
13093
|
-
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
13094
|
-
this.logger('error', `[Rejoin]: Rejoin ${this.reconnectAttempts} failed.`, err);
|
|
13095
|
-
await reconnect('full', 'previous attempt failed');
|
|
13096
|
-
this.logger('info', `[Rejoin]: Rejoin ${this.reconnectAttempts} successful!`);
|
|
13557
|
+
this.networkAvailableTask = networkAvailableTask;
|
|
13558
|
+
this.sfuStatsReporter?.stop();
|
|
13559
|
+
this.state.setCallingState(exports.CallingState.OFFLINE);
|
|
13097
13560
|
}
|
|
13098
13561
|
else {
|
|
13099
|
-
this.logger('
|
|
13100
|
-
this.
|
|
13101
|
-
|
|
13562
|
+
this.logger('debug', '[Reconnect] Going online');
|
|
13563
|
+
// TODO try to remove this .close call
|
|
13564
|
+
this.sfuClient?.close(4002, 'Closing WS to reconnect after going online');
|
|
13565
|
+
// we went online, release the previous waiters and reset the state
|
|
13566
|
+
this.networkAvailableTask?.resolve();
|
|
13567
|
+
this.networkAvailableTask = undefined;
|
|
13568
|
+
this.sfuStatsReporter?.start();
|
|
13569
|
+
}
|
|
13570
|
+
});
|
|
13571
|
+
this.leaveCallHooks.add(unregisterGoAway);
|
|
13572
|
+
this.leaveCallHooks.add(unregisterOnError);
|
|
13573
|
+
this.leaveCallHooks.add(unregisterNetworkChanged);
|
|
13574
|
+
};
|
|
13575
|
+
/**
|
|
13576
|
+
* Restores the published tracks after a reconnection.
|
|
13577
|
+
* @internal
|
|
13578
|
+
*/
|
|
13579
|
+
this.restorePublishedTracks = async () => {
|
|
13580
|
+
// the tracks need to be restored in their original order of publishing
|
|
13581
|
+
// otherwise, we might get `m-lines order mismatch` errors
|
|
13582
|
+
for (const trackType of this.trackPublishOrder) {
|
|
13583
|
+
switch (trackType) {
|
|
13584
|
+
case TrackType.AUDIO:
|
|
13585
|
+
const audioStream = this.microphone.state.mediaStream;
|
|
13586
|
+
if (audioStream) {
|
|
13587
|
+
await this.publishAudioStream(audioStream);
|
|
13588
|
+
}
|
|
13589
|
+
break;
|
|
13590
|
+
case TrackType.VIDEO:
|
|
13591
|
+
const videoStream = this.camera.state.mediaStream;
|
|
13592
|
+
if (videoStream) {
|
|
13593
|
+
await this.publishVideoStream(videoStream, {
|
|
13594
|
+
preferredCodec: this.camera.preferredCodec,
|
|
13595
|
+
});
|
|
13596
|
+
}
|
|
13597
|
+
break;
|
|
13598
|
+
case TrackType.SCREEN_SHARE:
|
|
13599
|
+
const screenShareStream = this.screenShare.state.mediaStream;
|
|
13600
|
+
if (screenShareStream) {
|
|
13601
|
+
await this.publishScreenShareStream(screenShareStream, {
|
|
13602
|
+
screenShareSettings: this.screenShare.getSettings(),
|
|
13603
|
+
});
|
|
13604
|
+
}
|
|
13605
|
+
break;
|
|
13606
|
+
// screen share audio can't exist without a screen share, so we handle it there
|
|
13607
|
+
case TrackType.SCREEN_SHARE_AUDIO:
|
|
13608
|
+
case TrackType.UNSPECIFIED:
|
|
13609
|
+
break;
|
|
13610
|
+
default:
|
|
13611
|
+
ensureExhausted(trackType, 'Unknown track type');
|
|
13612
|
+
break;
|
|
13102
13613
|
}
|
|
13103
13614
|
}
|
|
13104
13615
|
};
|
|
13105
|
-
|
|
13106
|
-
|
|
13107
|
-
|
|
13108
|
-
|
|
13109
|
-
|
|
13110
|
-
|
|
13111
|
-
|
|
13112
|
-
|
|
13113
|
-
|
|
13114
|
-
reject(new Error('Waiting for "joinResponse" has timed out'));
|
|
13115
|
-
}, timeout);
|
|
13116
|
-
});
|
|
13616
|
+
/**
|
|
13617
|
+
* Restores the subscribed tracks after a reconnection.
|
|
13618
|
+
* @internal
|
|
13619
|
+
*/
|
|
13620
|
+
this.restoreSubscribedTracks = () => {
|
|
13621
|
+
const { remoteParticipants } = this.state;
|
|
13622
|
+
if (remoteParticipants.length <= 0)
|
|
13623
|
+
return;
|
|
13624
|
+
this.updateSubscriptions(remoteParticipants, exports.DebounceType.FAST);
|
|
13117
13625
|
};
|
|
13118
13626
|
/**
|
|
13119
13627
|
* Starts publishing the given video stream to the call.
|
|
@@ -13128,7 +13636,7 @@ class Call {
|
|
|
13128
13636
|
this.publishVideoStream = async (videoStream, opts = {}) => {
|
|
13129
13637
|
// we should wait until we get a JoinResponse from the SFU,
|
|
13130
13638
|
// otherwise we risk breaking the ICETrickle flow.
|
|
13131
|
-
await this.
|
|
13639
|
+
await this.waitUntilCallJoined();
|
|
13132
13640
|
if (!this.publisher) {
|
|
13133
13641
|
this.logger('error', 'Trying to publish video before join is completed');
|
|
13134
13642
|
throw new Error(`Call not joined yet.`);
|
|
@@ -13138,6 +13646,9 @@ class Call {
|
|
|
13138
13646
|
this.logger('error', `There is no video track to publish in the stream.`);
|
|
13139
13647
|
return;
|
|
13140
13648
|
}
|
|
13649
|
+
if (!this.trackPublishOrder.includes(TrackType.VIDEO)) {
|
|
13650
|
+
this.trackPublishOrder.push(TrackType.VIDEO);
|
|
13651
|
+
}
|
|
13141
13652
|
await this.publisher.publishStream(videoStream, videoTrack, TrackType.VIDEO, opts);
|
|
13142
13653
|
};
|
|
13143
13654
|
/**
|
|
@@ -13152,7 +13663,7 @@ class Call {
|
|
|
13152
13663
|
this.publishAudioStream = async (audioStream) => {
|
|
13153
13664
|
// we should wait until we get a JoinResponse from the SFU,
|
|
13154
13665
|
// otherwise we risk breaking the ICETrickle flow.
|
|
13155
|
-
await this.
|
|
13666
|
+
await this.waitUntilCallJoined();
|
|
13156
13667
|
if (!this.publisher) {
|
|
13157
13668
|
this.logger('error', 'Trying to publish audio before join is completed');
|
|
13158
13669
|
throw new Error(`Call not joined yet.`);
|
|
@@ -13162,6 +13673,9 @@ class Call {
|
|
|
13162
13673
|
this.logger('error', `There is no audio track in the stream to publish`);
|
|
13163
13674
|
return;
|
|
13164
13675
|
}
|
|
13676
|
+
if (!this.trackPublishOrder.includes(TrackType.AUDIO)) {
|
|
13677
|
+
this.trackPublishOrder.push(TrackType.AUDIO);
|
|
13678
|
+
}
|
|
13165
13679
|
await this.publisher.publishStream(audioStream, audioTrack, TrackType.AUDIO);
|
|
13166
13680
|
};
|
|
13167
13681
|
/**
|
|
@@ -13176,7 +13690,7 @@ class Call {
|
|
|
13176
13690
|
this.publishScreenShareStream = async (screenShareStream, opts = {}) => {
|
|
13177
13691
|
// we should wait until we get a JoinResponse from the SFU,
|
|
13178
13692
|
// otherwise we risk breaking the ICETrickle flow.
|
|
13179
|
-
await this.
|
|
13693
|
+
await this.waitUntilCallJoined();
|
|
13180
13694
|
if (!this.publisher) {
|
|
13181
13695
|
this.logger('error', 'Trying to publish screen share before join is completed');
|
|
13182
13696
|
throw new Error(`Call not joined yet.`);
|
|
@@ -13186,9 +13700,15 @@ class Call {
|
|
|
13186
13700
|
this.logger('error', `There is no video track in the screen share stream to publish`);
|
|
13187
13701
|
return;
|
|
13188
13702
|
}
|
|
13703
|
+
if (!this.trackPublishOrder.includes(TrackType.SCREEN_SHARE)) {
|
|
13704
|
+
this.trackPublishOrder.push(TrackType.SCREEN_SHARE);
|
|
13705
|
+
}
|
|
13189
13706
|
await this.publisher.publishStream(screenShareStream, screenShareTrack, TrackType.SCREEN_SHARE, opts);
|
|
13190
13707
|
const [screenShareAudioTrack] = screenShareStream.getAudioTracks();
|
|
13191
13708
|
if (screenShareAudioTrack) {
|
|
13709
|
+
if (!this.trackPublishOrder.includes(TrackType.SCREEN_SHARE_AUDIO)) {
|
|
13710
|
+
this.trackPublishOrder.push(TrackType.SCREEN_SHARE_AUDIO);
|
|
13711
|
+
}
|
|
13192
13712
|
await this.publisher.publishStream(screenShareStream, screenShareAudioTrack, TrackType.SCREEN_SHARE_AUDIO, opts);
|
|
13193
13713
|
}
|
|
13194
13714
|
};
|
|
@@ -13233,19 +13753,9 @@ class Call {
|
|
|
13233
13753
|
* @param type the debounce type to use for the update.
|
|
13234
13754
|
*/
|
|
13235
13755
|
this.updateSubscriptionsPartial = (trackType, changes, type = exports.DebounceType.SLOW) => {
|
|
13236
|
-
if (trackType === 'video') {
|
|
13237
|
-
this.logger('warn', `updateSubscriptionsPartial: ${trackType} is deprecated. Please switch to 'videoTrack'`);
|
|
13238
|
-
trackType = 'videoTrack';
|
|
13239
|
-
}
|
|
13240
|
-
else if (trackType === 'screen') {
|
|
13241
|
-
this.logger('warn', `updateSubscriptionsPartial: ${trackType} is deprecated. Please switch to 'screenShareTrack'`);
|
|
13242
|
-
trackType = 'screenShareTrack';
|
|
13243
|
-
}
|
|
13244
13756
|
const participants = this.state.updateParticipants(Object.entries(changes).reduce((acc, [sessionId, change]) => {
|
|
13245
|
-
if (change.dimension
|
|
13757
|
+
if (change.dimension) {
|
|
13246
13758
|
change.dimension.height = Math.ceil(change.dimension.height);
|
|
13247
|
-
}
|
|
13248
|
-
if (change.dimension?.width) {
|
|
13249
13759
|
change.dimension.width = Math.ceil(change.dimension.width);
|
|
13250
13760
|
}
|
|
13251
13761
|
const prop = trackType === 'videoTrack'
|
|
@@ -13260,9 +13770,7 @@ class Call {
|
|
|
13260
13770
|
}
|
|
13261
13771
|
return acc;
|
|
13262
13772
|
}, {}));
|
|
13263
|
-
|
|
13264
|
-
this.updateSubscriptions(participants, type);
|
|
13265
|
-
}
|
|
13773
|
+
this.updateSubscriptions(participants, type);
|
|
13266
13774
|
};
|
|
13267
13775
|
this.updateSubscriptions = (participants, type = exports.DebounceType.SLOW) => {
|
|
13268
13776
|
const subscriptions = [];
|
|
@@ -13345,10 +13853,15 @@ class Call {
|
|
|
13345
13853
|
this.updatePublishQuality = async (enabledLayers) => {
|
|
13346
13854
|
return this.publisher?.updateVideoPublishQuality(enabledLayers);
|
|
13347
13855
|
};
|
|
13348
|
-
this.
|
|
13856
|
+
this.waitUntilCallJoined = () => {
|
|
13857
|
+
if (this.sfuClient) {
|
|
13858
|
+
// if we have an SFU client, we can wait for the join response
|
|
13859
|
+
return this.sfuClient.joinResponseTask.promise;
|
|
13860
|
+
}
|
|
13861
|
+
// otherwise, fall back to the calling state
|
|
13349
13862
|
return new Promise((resolve) => {
|
|
13350
13863
|
this.state.callingState$
|
|
13351
|
-
.pipe(rxjs.takeWhile((state) => state !== exports.CallingState.JOINED, true)
|
|
13864
|
+
.pipe(rxjs.takeWhile((state) => state !== exports.CallingState.JOINED, true))
|
|
13352
13865
|
.subscribe(() => resolve());
|
|
13353
13866
|
});
|
|
13354
13867
|
};
|
|
@@ -13732,14 +14245,72 @@ class Call {
|
|
|
13732
14245
|
*
|
|
13733
14246
|
* @internal
|
|
13734
14247
|
*/
|
|
13735
|
-
this.applyDeviceConfig = async () => {
|
|
13736
|
-
await this.initCamera({ setStatus:
|
|
14248
|
+
this.applyDeviceConfig = async (status) => {
|
|
14249
|
+
await this.initCamera({ setStatus: status }).catch((err) => {
|
|
13737
14250
|
this.logger('warn', 'Camera init failed', err);
|
|
13738
14251
|
});
|
|
13739
|
-
await this.initMic({ setStatus:
|
|
14252
|
+
await this.initMic({ setStatus: status }).catch((err) => {
|
|
13740
14253
|
this.logger('warn', 'Mic init failed', err);
|
|
13741
14254
|
});
|
|
13742
14255
|
};
|
|
14256
|
+
this.initCamera = async (options) => {
|
|
14257
|
+
// Wait for any in progress camera operation
|
|
14258
|
+
await this.camera.statusChangeSettled();
|
|
14259
|
+
if (this.state.localParticipant?.videoStream ||
|
|
14260
|
+
!this.permissionsContext.hasPermission('send-video')) {
|
|
14261
|
+
return;
|
|
14262
|
+
}
|
|
14263
|
+
// Set camera direction if it's not yet set
|
|
14264
|
+
if (!this.camera.state.direction && !this.camera.state.selectedDevice) {
|
|
14265
|
+
let defaultDirection = 'front';
|
|
14266
|
+
const backendSetting = this.state.settings?.video.camera_facing;
|
|
14267
|
+
if (backendSetting) {
|
|
14268
|
+
defaultDirection = backendSetting === 'front' ? 'front' : 'back';
|
|
14269
|
+
}
|
|
14270
|
+
this.camera.state.setDirection(defaultDirection);
|
|
14271
|
+
}
|
|
14272
|
+
// Set target resolution
|
|
14273
|
+
const targetResolution = this.state.settings?.video.target_resolution;
|
|
14274
|
+
if (targetResolution) {
|
|
14275
|
+
await this.camera.selectTargetResolution(targetResolution);
|
|
14276
|
+
}
|
|
14277
|
+
if (options.setStatus) {
|
|
14278
|
+
// Publish already that was set before we joined
|
|
14279
|
+
if (this.camera.enabled &&
|
|
14280
|
+
this.camera.state.mediaStream &&
|
|
14281
|
+
!this.publisher?.isPublishing(TrackType.VIDEO)) {
|
|
14282
|
+
await this.publishVideoStream(this.camera.state.mediaStream, {
|
|
14283
|
+
preferredCodec: this.camera.preferredCodec,
|
|
14284
|
+
});
|
|
14285
|
+
}
|
|
14286
|
+
// Start camera if backend config specifies, and there is no local setting
|
|
14287
|
+
if (this.camera.state.status === undefined &&
|
|
14288
|
+
this.state.settings?.video.camera_default_on) {
|
|
14289
|
+
await this.camera.enable();
|
|
14290
|
+
}
|
|
14291
|
+
}
|
|
14292
|
+
};
|
|
14293
|
+
this.initMic = async (options) => {
|
|
14294
|
+
// Wait for any in progress mic operation
|
|
14295
|
+
await this.microphone.statusChangeSettled();
|
|
14296
|
+
if (this.state.localParticipant?.audioStream ||
|
|
14297
|
+
!this.permissionsContext.hasPermission('send-audio')) {
|
|
14298
|
+
return;
|
|
14299
|
+
}
|
|
14300
|
+
if (options.setStatus) {
|
|
14301
|
+
// Publish media stream that was set before we joined
|
|
14302
|
+
if (this.microphone.enabled &&
|
|
14303
|
+
this.microphone.state.mediaStream &&
|
|
14304
|
+
!this.publisher?.isPublishing(TrackType.AUDIO)) {
|
|
14305
|
+
await this.publishAudioStream(this.microphone.state.mediaStream);
|
|
14306
|
+
}
|
|
14307
|
+
// Start mic if backend config specifies, and there is no local setting
|
|
14308
|
+
if (this.microphone.state.status === undefined &&
|
|
14309
|
+
this.state.settings?.audio.mic_default_on) {
|
|
14310
|
+
await this.microphone.enable();
|
|
14311
|
+
}
|
|
14312
|
+
}
|
|
14313
|
+
};
|
|
13743
14314
|
/**
|
|
13744
14315
|
* Will begin tracking the given element for visibility changes within the
|
|
13745
14316
|
* configured viewport element (`call.setViewport`).
|
|
@@ -13854,15 +14425,15 @@ class Call {
|
|
|
13854
14425
|
}
|
|
13855
14426
|
async setup() {
|
|
13856
14427
|
await withoutConcurrency(this.joinLeaveConcurrencyTag, async () => {
|
|
13857
|
-
if (this.initialized)
|
|
14428
|
+
if (this.initialized)
|
|
13858
14429
|
return;
|
|
13859
|
-
}
|
|
13860
14430
|
this.leaveCallHooks.add(this.on('all', (event) => {
|
|
13861
14431
|
// update state with the latest event data
|
|
13862
14432
|
this.state.updateFromEvent(event);
|
|
13863
14433
|
}));
|
|
13864
|
-
this.leaveCallHooks.add(registerEventHandlers(this, this.
|
|
14434
|
+
this.leaveCallHooks.add(registerEventHandlers(this, this.dispatcher));
|
|
13865
14435
|
this.registerEffects();
|
|
14436
|
+
this.registerReconnectHandlers();
|
|
13866
14437
|
this.leaveCallHooks.add(createSubscription(this.trackSubscriptionsSubject.pipe(rxjs.debounce((v) => rxjs.timer(v.type)), rxjs.map((v) => v.data)), (subscriptions) => this.sfuClient?.updateSubscriptions(subscriptions).catch((err) => {
|
|
13867
14438
|
this.logger('debug', `Failed to update track subscriptions`, err);
|
|
13868
14439
|
})));
|
|
@@ -13882,44 +14453,7 @@ class Call {
|
|
|
13882
14453
|
}));
|
|
13883
14454
|
this.leaveCallHooks.add(
|
|
13884
14455
|
// handle the case when the user permissions are modified.
|
|
13885
|
-
|
|
13886
|
-
// update the permission context.
|
|
13887
|
-
this.permissionsContext.setPermissions(ownCapabilities);
|
|
13888
|
-
if (!this.publisher)
|
|
13889
|
-
return;
|
|
13890
|
-
// check if the user still has publishing permissions and stop publishing if not.
|
|
13891
|
-
const permissionToTrackType = {
|
|
13892
|
-
[OwnCapability.SEND_AUDIO]: TrackType.AUDIO,
|
|
13893
|
-
[OwnCapability.SEND_VIDEO]: TrackType.VIDEO,
|
|
13894
|
-
[OwnCapability.SCREENSHARE]: TrackType.SCREEN_SHARE,
|
|
13895
|
-
};
|
|
13896
|
-
for (const [permission, trackType] of Object.entries(permissionToTrackType)) {
|
|
13897
|
-
const hasPermission = this.permissionsContext.hasPermission(permission);
|
|
13898
|
-
if (!hasPermission &&
|
|
13899
|
-
(this.publisher.isPublishing(trackType) ||
|
|
13900
|
-
this.publisher.isLive(trackType))) {
|
|
13901
|
-
// Stop tracks, then notify device manager
|
|
13902
|
-
this.stopPublish(trackType)
|
|
13903
|
-
.catch((err) => {
|
|
13904
|
-
this.logger('error', `Error stopping publish ${trackType}`, err);
|
|
13905
|
-
})
|
|
13906
|
-
.then(() => {
|
|
13907
|
-
if (trackType === TrackType.VIDEO &&
|
|
13908
|
-
this.camera.state.status === 'enabled') {
|
|
13909
|
-
this.camera
|
|
13910
|
-
.disable()
|
|
13911
|
-
.catch((err) => this.logger('error', `Error disabling camera after permission revoked`, err));
|
|
13912
|
-
}
|
|
13913
|
-
if (trackType === TrackType.AUDIO &&
|
|
13914
|
-
this.microphone.state.status === 'enabled') {
|
|
13915
|
-
this.microphone
|
|
13916
|
-
.disable()
|
|
13917
|
-
.catch((err) => this.logger('error', `Error disabling microphone after permission revoked`, err));
|
|
13918
|
-
}
|
|
13919
|
-
});
|
|
13920
|
-
}
|
|
13921
|
-
}
|
|
13922
|
-
}));
|
|
14456
|
+
createSafeAsyncSubscription(this.state.ownCapabilities$, this.handleOwnCapabilitiesUpdated));
|
|
13923
14457
|
this.leaveCallHooks.add(
|
|
13924
14458
|
// handles the case when the user is blocked by the call owner.
|
|
13925
14459
|
createSubscription(this.state.blockedUserIds$, async (blockedUserIds) => {
|
|
@@ -14011,63 +14545,20 @@ class Call {
|
|
|
14011
14545
|
get isCreatedByMe() {
|
|
14012
14546
|
return this.state.createdBy?.id === this.currentUserId;
|
|
14013
14547
|
}
|
|
14014
|
-
|
|
14015
|
-
|
|
14016
|
-
|
|
14017
|
-
|
|
14018
|
-
|
|
14019
|
-
|
|
14020
|
-
|
|
14021
|
-
|
|
14022
|
-
|
|
14023
|
-
|
|
14024
|
-
const backendSetting = this.state.settings?.video.camera_facing;
|
|
14025
|
-
if (backendSetting) {
|
|
14026
|
-
defaultDirection = backendSetting === 'front' ? 'front' : 'back';
|
|
14027
|
-
}
|
|
14028
|
-
this.camera.state.setDirection(defaultDirection);
|
|
14029
|
-
}
|
|
14030
|
-
// Set target resolution
|
|
14031
|
-
const targetResolution = this.state.settings?.video.target_resolution;
|
|
14032
|
-
if (targetResolution) {
|
|
14033
|
-
await this.camera.selectTargetResolution(targetResolution);
|
|
14034
|
-
}
|
|
14035
|
-
if (options.setStatus) {
|
|
14036
|
-
// Publish already that was set before we joined
|
|
14037
|
-
if (this.camera.state.status === 'enabled' &&
|
|
14038
|
-
this.camera.state.mediaStream &&
|
|
14039
|
-
!this.publisher?.isPublishing(TrackType.VIDEO)) {
|
|
14040
|
-
await this.publishVideoStream(this.camera.state.mediaStream, {
|
|
14041
|
-
preferredCodec: this.camera.preferredCodec,
|
|
14042
|
-
});
|
|
14043
|
-
}
|
|
14044
|
-
// Start camera if backend config specifies, and there is no local setting
|
|
14045
|
-
if (this.camera.state.status === undefined &&
|
|
14046
|
-
this.state.settings?.video.camera_default_on) {
|
|
14047
|
-
await this.camera.enable();
|
|
14048
|
-
}
|
|
14049
|
-
}
|
|
14050
|
-
}
|
|
14051
|
-
async initMic(options) {
|
|
14052
|
-
// Wait for any in progress mic operation
|
|
14053
|
-
await this.microphone.statusChangeSettled();
|
|
14054
|
-
if (this.state.localParticipant?.audioStream ||
|
|
14055
|
-
!this.permissionsContext.hasPermission('send-audio')) {
|
|
14548
|
+
/**
|
|
14549
|
+
* Handles the closing of the SFU signal connection.
|
|
14550
|
+
*
|
|
14551
|
+
* @internal
|
|
14552
|
+
* @param sfuClient the SFU client instance that was closed.
|
|
14553
|
+
*/
|
|
14554
|
+
handleSfuSignalClose(sfuClient) {
|
|
14555
|
+
this.logger('debug', '[Reconnect] SFU signal connection closed');
|
|
14556
|
+
// normal close, no need to reconnect
|
|
14557
|
+
if (sfuClient.isLeaving)
|
|
14056
14558
|
return;
|
|
14057
|
-
|
|
14058
|
-
|
|
14059
|
-
|
|
14060
|
-
if (this.microphone.state.status === 'enabled' &&
|
|
14061
|
-
this.microphone.state.mediaStream &&
|
|
14062
|
-
!this.publisher?.isPublishing(TrackType.AUDIO)) {
|
|
14063
|
-
await this.publishAudioStream(this.microphone.state.mediaStream);
|
|
14064
|
-
}
|
|
14065
|
-
// Start mic if backend config specifies, and there is no local setting
|
|
14066
|
-
if (this.microphone.state.status === undefined &&
|
|
14067
|
-
this.state.settings?.audio.mic_default_on) {
|
|
14068
|
-
await this.microphone.enable();
|
|
14069
|
-
}
|
|
14070
|
-
}
|
|
14559
|
+
this.reconnect(WebsocketReconnectStrategy.REJOIN).catch((err) => {
|
|
14560
|
+
this.logger('warn', '[Reconnect] Error reconnecting', err);
|
|
14561
|
+
});
|
|
14071
14562
|
}
|
|
14072
14563
|
}
|
|
14073
14564
|
|
|
@@ -14270,6 +14761,7 @@ class StableWSConnection {
|
|
|
14270
14761
|
}
|
|
14271
14762
|
}
|
|
14272
14763
|
if (data) {
|
|
14764
|
+
data.received_at = new Date();
|
|
14273
14765
|
this.client.dispatchEvent(data);
|
|
14274
14766
|
}
|
|
14275
14767
|
this.scheduleConnectionCheck();
|
|
@@ -14610,7 +15102,7 @@ class StableWSConnection {
|
|
|
14610
15102
|
wsURL,
|
|
14611
15103
|
requestID: this.requestID,
|
|
14612
15104
|
});
|
|
14613
|
-
this.ws = new WebSocket(wsURL);
|
|
15105
|
+
this.ws = new WebSocket$1(wsURL);
|
|
14614
15106
|
this.ws.onopen = this.onopen.bind(this, this.wsID);
|
|
14615
15107
|
this.ws.onclose = this.onclose.bind(this, this.wsID);
|
|
14616
15108
|
this.ws.onerror = this.onerror.bind(this, this.wsID);
|
|
@@ -15226,6 +15718,7 @@ class StreamClient {
|
|
|
15226
15718
|
const wsPromise = this.openConnection();
|
|
15227
15719
|
this.setUserPromise = Promise.all([setTokenPromise, wsPromise]).then((result) => result[1]);
|
|
15228
15720
|
try {
|
|
15721
|
+
addConnectionEventListeners(this.updateNetworkConnectionStatus);
|
|
15229
15722
|
return await this.setUserPromise;
|
|
15230
15723
|
}
|
|
15231
15724
|
catch (err) {
|
|
@@ -15321,6 +15814,7 @@ class StreamClient {
|
|
|
15321
15814
|
delete this.userID;
|
|
15322
15815
|
this.anonymous = false;
|
|
15323
15816
|
await this.closeConnection(timeout);
|
|
15817
|
+
removeConnectionEventListeners(this.updateNetworkConnectionStatus);
|
|
15324
15818
|
this.tokenManager.reset();
|
|
15325
15819
|
this.connectionIdPromise = undefined;
|
|
15326
15820
|
this.rejectConnectionId = undefined;
|
|
@@ -15340,6 +15834,7 @@ class StreamClient {
|
|
|
15340
15834
|
* connectAnonymousUser - Set an anonymous user and open a WebSocket connection
|
|
15341
15835
|
*/
|
|
15342
15836
|
this.connectAnonymousUser = async (user, tokenOrProvider) => {
|
|
15837
|
+
addConnectionEventListeners(this.updateNetworkConnectionStatus);
|
|
15343
15838
|
this.connectionIdPromise = new Promise((resolve, reject) => {
|
|
15344
15839
|
this.resolveConnectionId = resolve;
|
|
15345
15840
|
this.rejectConnectionId = reject;
|
|
@@ -15499,8 +15994,6 @@ class StreamClient {
|
|
|
15499
15994
|
return data;
|
|
15500
15995
|
};
|
|
15501
15996
|
this.dispatchEvent = (event) => {
|
|
15502
|
-
if (!event.received_at)
|
|
15503
|
-
event.received_at = new Date();
|
|
15504
15997
|
this.logger('debug', `Dispatching event: ${event.type}`, event);
|
|
15505
15998
|
if (!this.listeners)
|
|
15506
15999
|
return;
|
|
@@ -15591,7 +16084,7 @@ class StreamClient {
|
|
|
15591
16084
|
});
|
|
15592
16085
|
};
|
|
15593
16086
|
this.getUserAgent = () => {
|
|
15594
|
-
const version = "1.
|
|
16087
|
+
const version = "1.6.0-0" ;
|
|
15595
16088
|
return (this.userAgent ||
|
|
15596
16089
|
`stream-video-javascript-client-${this.node ? 'node' : 'browser'}-${version}`);
|
|
15597
16090
|
};
|
|
@@ -15657,11 +16150,15 @@ class StreamClient {
|
|
|
15657
16150
|
client_request_id,
|
|
15658
16151
|
});
|
|
15659
16152
|
};
|
|
15660
|
-
|
|
15661
|
-
|
|
15662
|
-
|
|
15663
|
-
|
|
15664
|
-
|
|
16153
|
+
this.updateNetworkConnectionStatus = (event) => {
|
|
16154
|
+
if (event.type === 'offline') {
|
|
16155
|
+
this.logger('debug', 'device went offline');
|
|
16156
|
+
this.dispatchEvent({ type: 'network.changed', online: false });
|
|
16157
|
+
}
|
|
16158
|
+
else if (event.type === 'online') {
|
|
16159
|
+
this.logger('debug', 'device went online');
|
|
16160
|
+
this.dispatchEvent({ type: 'network.changed', online: true });
|
|
16161
|
+
}
|
|
15665
16162
|
};
|
|
15666
16163
|
// set the key
|
|
15667
16164
|
this.key = key;
|
|
@@ -15691,10 +16188,14 @@ class StreamClient {
|
|
|
15691
16188
|
});
|
|
15692
16189
|
}
|
|
15693
16190
|
this.setBaseURL(this.options.baseURL || 'https://video.stream-io-api.com/video');
|
|
15694
|
-
if (typeof process !== 'undefined' &&
|
|
16191
|
+
if (typeof process !== 'undefined' &&
|
|
16192
|
+
'env' in process &&
|
|
16193
|
+
process.env.STREAM_LOCAL_TEST_RUN) {
|
|
15695
16194
|
this.setBaseURL('http://localhost:3030/video');
|
|
15696
16195
|
}
|
|
15697
|
-
if (typeof process !== 'undefined' &&
|
|
16196
|
+
if (typeof process !== 'undefined' &&
|
|
16197
|
+
'env' in process &&
|
|
16198
|
+
process.env.STREAM_LOCAL_TEST_HOST) {
|
|
15698
16199
|
this.setBaseURL(`http://${process.env.STREAM_LOCAL_TEST_HOST}/video`);
|
|
15699
16200
|
}
|
|
15700
16201
|
this.axiosInstance = axios.create({
|
|
@@ -15729,6 +16230,99 @@ class StreamVideoClient {
|
|
|
15729
16230
|
constructor(apiKeyOrArgs, opts) {
|
|
15730
16231
|
this.logLevel = 'warn';
|
|
15731
16232
|
this.eventHandlersToUnregister = [];
|
|
16233
|
+
/**
|
|
16234
|
+
* Connects the given user to the client.
|
|
16235
|
+
* Only one user can connect at a time, if you want to change users, call `disconnectUser` before connecting a new user.
|
|
16236
|
+
* If the connection is successful, the connected user [state variable](#readonlystatestore) will be updated accordingly.
|
|
16237
|
+
*
|
|
16238
|
+
* @param user the user to connect.
|
|
16239
|
+
* @param token a token or a function that returns a token.
|
|
16240
|
+
*/
|
|
16241
|
+
this.connectUser = async (user, token) => {
|
|
16242
|
+
if (user.type === 'anonymous') {
|
|
16243
|
+
user.id = '!anon';
|
|
16244
|
+
return this.connectAnonymousUser(user, token);
|
|
16245
|
+
}
|
|
16246
|
+
let connectUser = () => {
|
|
16247
|
+
return this.streamClient.connectUser(user, token);
|
|
16248
|
+
};
|
|
16249
|
+
if (user.type === 'guest') {
|
|
16250
|
+
connectUser = async () => {
|
|
16251
|
+
return this.streamClient.connectGuestUser(user);
|
|
16252
|
+
};
|
|
16253
|
+
}
|
|
16254
|
+
this.connectionPromise = this.disconnectionPromise
|
|
16255
|
+
? this.disconnectionPromise.then(() => connectUser())
|
|
16256
|
+
: connectUser();
|
|
16257
|
+
this.connectionPromise?.finally(() => (this.connectionPromise = undefined));
|
|
16258
|
+
const connectUserResponse = await this.connectionPromise;
|
|
16259
|
+
// connectUserResponse will be void if connectUser called twice for the same user
|
|
16260
|
+
if (connectUserResponse?.me) {
|
|
16261
|
+
this.writeableStateStore.setConnectedUser(connectUserResponse.me);
|
|
16262
|
+
}
|
|
16263
|
+
this.eventHandlersToUnregister.push(this.on('connection.changed', (event) => {
|
|
16264
|
+
if (event.online) {
|
|
16265
|
+
const callsToReWatch = this.writeableStateStore.calls
|
|
16266
|
+
.filter((call) => call.watching)
|
|
16267
|
+
.map((call) => call.cid);
|
|
16268
|
+
this.logger('info', `Rewatching calls after connection changed ${callsToReWatch.join(', ')}`);
|
|
16269
|
+
if (callsToReWatch.length > 0) {
|
|
16270
|
+
this.queryCalls({
|
|
16271
|
+
watch: true,
|
|
16272
|
+
filter_conditions: {
|
|
16273
|
+
cid: { $in: callsToReWatch },
|
|
16274
|
+
},
|
|
16275
|
+
sort: [{ field: 'cid', direction: 1 }],
|
|
16276
|
+
}).catch((err) => {
|
|
16277
|
+
this.logger('error', 'Failed to re-watch calls', err);
|
|
16278
|
+
});
|
|
16279
|
+
}
|
|
16280
|
+
}
|
|
16281
|
+
}));
|
|
16282
|
+
this.eventHandlersToUnregister.push(this.on('call.created', (event) => {
|
|
16283
|
+
const { call, members } = event;
|
|
16284
|
+
if (user.id === call.created_by.id) {
|
|
16285
|
+
this.logger('warn', 'Received `call.created` sent by the current user');
|
|
16286
|
+
return;
|
|
16287
|
+
}
|
|
16288
|
+
this.logger('info', `New call created and registered: ${call.cid}`);
|
|
16289
|
+
const newCall = new Call({
|
|
16290
|
+
streamClient: this.streamClient,
|
|
16291
|
+
type: call.type,
|
|
16292
|
+
id: call.id,
|
|
16293
|
+
members,
|
|
16294
|
+
clientStore: this.writeableStateStore,
|
|
16295
|
+
});
|
|
16296
|
+
newCall.state.updateFromCallResponse(call);
|
|
16297
|
+
this.writeableStateStore.registerCall(newCall);
|
|
16298
|
+
}));
|
|
16299
|
+
this.eventHandlersToUnregister.push(this.on('call.ring', async (event) => {
|
|
16300
|
+
const { call, members } = event;
|
|
16301
|
+
if (user.id === call.created_by.id) {
|
|
16302
|
+
this.logger('debug', 'Received `call.ring` sent by the current user so ignoring the event');
|
|
16303
|
+
return;
|
|
16304
|
+
}
|
|
16305
|
+
// The call might already be tracked by the client,
|
|
16306
|
+
// if `call.created` was received before `call.ring`.
|
|
16307
|
+
// In that case, we cleanup the already tracked call.
|
|
16308
|
+
const prevCall = this.writeableStateStore.findCall(call.type, call.id);
|
|
16309
|
+
await prevCall?.leave({ reason: 'cleaning-up in call.ring' });
|
|
16310
|
+
// we create a new call
|
|
16311
|
+
const theCall = new Call({
|
|
16312
|
+
streamClient: this.streamClient,
|
|
16313
|
+
type: call.type,
|
|
16314
|
+
id: call.id,
|
|
16315
|
+
members,
|
|
16316
|
+
clientStore: this.writeableStateStore,
|
|
16317
|
+
ringing: true,
|
|
16318
|
+
});
|
|
16319
|
+
theCall.state.updateFromCallResponse(call);
|
|
16320
|
+
// we fetch the latest metadata for the call from the server
|
|
16321
|
+
await theCall.get();
|
|
16322
|
+
this.writeableStateStore.registerCall(theCall);
|
|
16323
|
+
}));
|
|
16324
|
+
return connectUserResponse;
|
|
16325
|
+
};
|
|
15732
16326
|
/**
|
|
15733
16327
|
* Disconnects the currently connected user from the client.
|
|
15734
16328
|
*
|
|
@@ -15817,7 +16411,7 @@ class StreamVideoClient {
|
|
|
15817
16411
|
clientStore: this.writeableStateStore,
|
|
15818
16412
|
});
|
|
15819
16413
|
call.state.updateFromCallResponse(c.call);
|
|
15820
|
-
await call.applyDeviceConfig();
|
|
16414
|
+
await call.applyDeviceConfig(false);
|
|
15821
16415
|
if (data.watch) {
|
|
15822
16416
|
this.writeableStateStore.registerCall(call);
|
|
15823
16417
|
}
|
|
@@ -15861,6 +16455,17 @@ class StreamVideoClient {
|
|
|
15861
16455
|
...(push_provider_name != null ? { push_provider_name } : {}),
|
|
15862
16456
|
});
|
|
15863
16457
|
};
|
|
16458
|
+
/**
|
|
16459
|
+
* addDevice - Adds a push device for a user.
|
|
16460
|
+
*
|
|
16461
|
+
* @param {string} id the device id
|
|
16462
|
+
* @param {string} push_provider the push provider name (eg. apn, firebase)
|
|
16463
|
+
* @param {string} push_provider_name user provided push provider name
|
|
16464
|
+
* @param {string} [userID] the user id (defaults to current user)
|
|
16465
|
+
*/
|
|
16466
|
+
this.addVoipDevice = async (id, push_provider, push_provider_name, userID) => {
|
|
16467
|
+
return await this.addDevice(id, push_provider, push_provider_name, userID, true);
|
|
16468
|
+
};
|
|
15864
16469
|
/**
|
|
15865
16470
|
* getDevices - Returns the devices associated with a current user
|
|
15866
16471
|
* @param {string} [userID] User ID. Only works on serverside
|
|
@@ -15888,7 +16493,7 @@ class StreamVideoClient {
|
|
|
15888
16493
|
this.onRingingCall = async (call_cid) => {
|
|
15889
16494
|
// if we find the call and is already ringing, we don't need to create a new call
|
|
15890
16495
|
// as client would have received the call.ring state because the app had WS alive when receiving push notifications
|
|
15891
|
-
let call = this.
|
|
16496
|
+
let call = this.state.calls.find((c) => c.cid === call_cid && c.ringing);
|
|
15892
16497
|
if (!call) {
|
|
15893
16498
|
// if not it means that WS is not alive when receiving the push notifications and we need to fetch the call
|
|
15894
16499
|
const [callType, callId] = call_cid.split(':');
|
|
@@ -15929,12 +16534,13 @@ class StreamVideoClient {
|
|
|
15929
16534
|
}
|
|
15930
16535
|
setLogger(logger, logLevel);
|
|
15931
16536
|
this.logger = getLogger(['client']);
|
|
16537
|
+
const coordinatorLogger = getLogger(['coordinator']);
|
|
15932
16538
|
if (typeof apiKeyOrArgs === 'string') {
|
|
15933
16539
|
this.streamClient = new StreamClient(apiKeyOrArgs, {
|
|
15934
16540
|
persistUserOnConnectionFailure: true,
|
|
15935
16541
|
...opts,
|
|
15936
16542
|
logLevel,
|
|
15937
|
-
logger:
|
|
16543
|
+
logger: coordinatorLogger,
|
|
15938
16544
|
});
|
|
15939
16545
|
}
|
|
15940
16546
|
else {
|
|
@@ -15942,12 +16548,14 @@ class StreamVideoClient {
|
|
|
15942
16548
|
persistUserOnConnectionFailure: true,
|
|
15943
16549
|
...apiKeyOrArgs.options,
|
|
15944
16550
|
logLevel,
|
|
15945
|
-
logger:
|
|
16551
|
+
logger: coordinatorLogger,
|
|
15946
16552
|
});
|
|
15947
16553
|
const sdkInfo = getSdkInfo();
|
|
15948
16554
|
if (sdkInfo) {
|
|
15949
|
-
|
|
15950
|
-
|
|
16555
|
+
const sdkName = SdkType[sdkInfo.type].toLowerCase();
|
|
16556
|
+
const sdkVersion = `${sdkInfo.major}.${sdkInfo.minor}.${sdkInfo.patch}`;
|
|
16557
|
+
const userAgent = this.streamClient.getUserAgent();
|
|
16558
|
+
this.streamClient.setUserAgent(`${userAgent}-video-${sdkName}-sdk-${sdkVersion}`);
|
|
15951
16559
|
}
|
|
15952
16560
|
}
|
|
15953
16561
|
this.writeableStateStore = new StreamVideoWriteableStateStore();
|
|
@@ -16000,110 +16608,6 @@ class StreamVideoClient {
|
|
|
16000
16608
|
get state() {
|
|
16001
16609
|
return this.readOnlyStateStore;
|
|
16002
16610
|
}
|
|
16003
|
-
/**
|
|
16004
|
-
* Connects the given user to the client.
|
|
16005
|
-
* Only one user can connect at a time, if you want to change users, call `disconnectUser` before connecting a new user.
|
|
16006
|
-
* If the connection is successful, the connected user [state variable](#readonlystatestore) will be updated accordingly.
|
|
16007
|
-
*
|
|
16008
|
-
* @param user the user to connect.
|
|
16009
|
-
* @param token a token or a function that returns a token.
|
|
16010
|
-
*/
|
|
16011
|
-
async connectUser(user, token) {
|
|
16012
|
-
if (user.type === 'anonymous') {
|
|
16013
|
-
user.id = '!anon';
|
|
16014
|
-
return this.connectAnonymousUser(user, token);
|
|
16015
|
-
}
|
|
16016
|
-
let connectUser = () => {
|
|
16017
|
-
return this.streamClient.connectUser(user, token);
|
|
16018
|
-
};
|
|
16019
|
-
if (user.type === 'guest') {
|
|
16020
|
-
connectUser = async () => {
|
|
16021
|
-
return this.streamClient.connectGuestUser(user);
|
|
16022
|
-
};
|
|
16023
|
-
}
|
|
16024
|
-
this.connectionPromise = this.disconnectionPromise
|
|
16025
|
-
? this.disconnectionPromise.then(() => connectUser())
|
|
16026
|
-
: connectUser();
|
|
16027
|
-
this.connectionPromise?.finally(() => (this.connectionPromise = undefined));
|
|
16028
|
-
const connectUserResponse = await this.connectionPromise;
|
|
16029
|
-
// connectUserResponse will be void if connectUser called twice for the same user
|
|
16030
|
-
if (connectUserResponse?.me) {
|
|
16031
|
-
this.writeableStateStore.setConnectedUser(connectUserResponse.me);
|
|
16032
|
-
}
|
|
16033
|
-
this.eventHandlersToUnregister.push(this.on('connection.changed', (event) => {
|
|
16034
|
-
if (event.online) {
|
|
16035
|
-
const callsToReWatch = this.writeableStateStore.calls
|
|
16036
|
-
.filter((call) => call.watching)
|
|
16037
|
-
.map((call) => call.cid);
|
|
16038
|
-
this.logger('info', `Rewatching calls after connection changed ${callsToReWatch.join(', ')}`);
|
|
16039
|
-
if (callsToReWatch.length > 0) {
|
|
16040
|
-
this.queryCalls({
|
|
16041
|
-
watch: true,
|
|
16042
|
-
filter_conditions: {
|
|
16043
|
-
cid: { $in: callsToReWatch },
|
|
16044
|
-
},
|
|
16045
|
-
sort: [{ field: 'cid', direction: 1 }],
|
|
16046
|
-
}).catch((err) => {
|
|
16047
|
-
this.logger('error', 'Failed to re-watch calls', err);
|
|
16048
|
-
});
|
|
16049
|
-
}
|
|
16050
|
-
}
|
|
16051
|
-
}));
|
|
16052
|
-
this.eventHandlersToUnregister.push(this.on('call.created', (event) => {
|
|
16053
|
-
const { call, members } = event;
|
|
16054
|
-
if (user.id === call.created_by.id) {
|
|
16055
|
-
this.logger('warn', 'Received `call.created` sent by the current user');
|
|
16056
|
-
return;
|
|
16057
|
-
}
|
|
16058
|
-
this.logger('info', `New call created and registered: ${call.cid}`);
|
|
16059
|
-
const newCall = new Call({
|
|
16060
|
-
streamClient: this.streamClient,
|
|
16061
|
-
type: call.type,
|
|
16062
|
-
id: call.id,
|
|
16063
|
-
members,
|
|
16064
|
-
clientStore: this.writeableStateStore,
|
|
16065
|
-
});
|
|
16066
|
-
newCall.state.updateFromCallResponse(call);
|
|
16067
|
-
this.writeableStateStore.registerCall(newCall);
|
|
16068
|
-
}));
|
|
16069
|
-
this.eventHandlersToUnregister.push(this.on('call.ring', async (event) => {
|
|
16070
|
-
const { call, members } = event;
|
|
16071
|
-
if (user.id === call.created_by.id) {
|
|
16072
|
-
this.logger('debug', 'Received `call.ring` sent by the current user so ignoring the event');
|
|
16073
|
-
return;
|
|
16074
|
-
}
|
|
16075
|
-
// The call might already be tracked by the client,
|
|
16076
|
-
// if `call.created` was received before `call.ring`.
|
|
16077
|
-
// In that case, we cleanup the already tracked call.
|
|
16078
|
-
const prevCall = this.writeableStateStore.findCall(call.type, call.id);
|
|
16079
|
-
await prevCall?.leave({ reason: 'cleaning-up in call.ring' });
|
|
16080
|
-
// we create a new call
|
|
16081
|
-
const theCall = new Call({
|
|
16082
|
-
streamClient: this.streamClient,
|
|
16083
|
-
type: call.type,
|
|
16084
|
-
id: call.id,
|
|
16085
|
-
members,
|
|
16086
|
-
clientStore: this.writeableStateStore,
|
|
16087
|
-
ringing: true,
|
|
16088
|
-
});
|
|
16089
|
-
theCall.state.updateFromCallResponse(call);
|
|
16090
|
-
// we fetch the latest metadata for the call from the server
|
|
16091
|
-
await theCall.get();
|
|
16092
|
-
this.writeableStateStore.registerCall(theCall);
|
|
16093
|
-
}));
|
|
16094
|
-
return connectUserResponse;
|
|
16095
|
-
}
|
|
16096
|
-
/**
|
|
16097
|
-
* addDevice - Adds a push device for a user.
|
|
16098
|
-
*
|
|
16099
|
-
* @param {string} id the device id
|
|
16100
|
-
* @param {string} push_provider the push provider name (eg. apn, firebase)
|
|
16101
|
-
* @param {string} push_provider_name user provided push provider name
|
|
16102
|
-
* @param {string} [userID] the user id (defaults to current user)
|
|
16103
|
-
*/
|
|
16104
|
-
async addVoipDevice(id, push_provider, push_provider_name, userID) {
|
|
16105
|
-
return await this.addDevice(id, push_provider, push_provider_name, userID, true);
|
|
16106
|
-
}
|
|
16107
16611
|
}
|
|
16108
16612
|
StreamVideoClient._instanceMap = new Map();
|
|
16109
16613
|
|
|
@@ -16166,6 +16670,7 @@ exports.getAudioOutputDevices = getAudioOutputDevices;
|
|
|
16166
16670
|
exports.getAudioStream = getAudioStream;
|
|
16167
16671
|
exports.getClientDetails = getClientDetails;
|
|
16168
16672
|
exports.getDeviceInfo = getDeviceInfo;
|
|
16673
|
+
exports.getLogLevel = getLogLevel;
|
|
16169
16674
|
exports.getLogger = getLogger;
|
|
16170
16675
|
exports.getOSInfo = getOSInfo;
|
|
16171
16676
|
exports.getScreenShareStream = getScreenShareStream;
|