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