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