@stream-io/video-client 1.5.0-0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -230
- package/dist/index.browser.es.js +1498 -1963
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +1495 -1961
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +1498 -1963
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +9 -93
- package/dist/src/StreamSfuClient.d.ts +56 -72
- package/dist/src/StreamVideoClient.d.ts +10 -2
- package/dist/src/coordinator/connection/client.d.ts +4 -3
- package/dist/src/coordinator/connection/types.d.ts +1 -5
- package/dist/src/devices/InputMediaDeviceManager.d.ts +0 -4
- package/dist/src/devices/MicrophoneManager.d.ts +1 -1
- package/dist/src/events/callEventHandlers.d.ts +3 -1
- package/dist/src/events/internal.d.ts +0 -4
- package/dist/src/gen/video/sfu/event/events.d.ts +4 -106
- package/dist/src/gen/video/sfu/models/models.d.ts +65 -64
- package/dist/src/logger.d.ts +0 -1
- package/dist/src/rpc/createClient.d.ts +0 -2
- package/dist/src/rpc/index.d.ts +0 -1
- package/dist/src/rtc/Dispatcher.d.ts +1 -1
- package/dist/src/rtc/IceTrickleBuffer.d.ts +1 -0
- package/dist/src/rtc/Publisher.d.ts +25 -24
- package/dist/src/rtc/Subscriber.d.ts +11 -12
- package/dist/src/rtc/flows/join.d.ts +20 -0
- package/dist/src/rtc/helpers/tracks.d.ts +3 -3
- package/dist/src/rtc/signal.d.ts +1 -1
- package/dist/src/store/CallState.d.ts +2 -46
- package/package.json +3 -3
- package/src/Call.ts +562 -615
- package/src/StreamSfuClient.ts +246 -277
- package/src/StreamVideoClient.ts +65 -15
- package/src/coordinator/connection/client.ts +8 -25
- package/src/coordinator/connection/connection.ts +0 -1
- package/src/coordinator/connection/token_manager.ts +1 -1
- package/src/coordinator/connection/types.ts +0 -6
- package/src/devices/BrowserPermission.ts +1 -5
- package/src/devices/CameraManager.ts +1 -1
- package/src/devices/InputMediaDeviceManager.ts +3 -12
- package/src/devices/MicrophoneManager.ts +3 -3
- package/src/devices/devices.ts +1 -1
- package/src/events/__tests__/mutes.test.ts +13 -10
- package/src/events/__tests__/participant.test.ts +0 -75
- package/src/events/callEventHandlers.ts +7 -4
- package/src/events/internal.ts +3 -20
- package/src/events/mutes.ts +3 -5
- package/src/events/participant.ts +15 -48
- package/src/gen/video/sfu/event/events.ts +8 -451
- package/src/gen/video/sfu/models/models.ts +204 -211
- package/src/logger.ts +1 -3
- package/src/rpc/createClient.ts +0 -21
- package/src/rpc/index.ts +0 -1
- package/src/rtc/Dispatcher.ts +2 -6
- package/src/rtc/IceTrickleBuffer.ts +2 -2
- package/src/rtc/Publisher.ts +163 -127
- package/src/rtc/Subscriber.ts +155 -94
- package/src/rtc/__tests__/Publisher.test.ts +95 -18
- package/src/rtc/__tests__/Subscriber.test.ts +99 -63
- package/src/rtc/__tests__/videoLayers.test.ts +2 -2
- package/src/rtc/flows/join.ts +65 -0
- package/src/rtc/helpers/tracks.ts +7 -27
- package/src/rtc/signal.ts +3 -3
- package/src/rtc/videoLayers.ts +10 -1
- package/src/stats/SfuStatsReporter.ts +0 -1
- package/src/store/CallState.ts +2 -109
- package/src/store/__tests__/CallState.test.ts +37 -48
- package/dist/src/helpers/ensureExhausted.d.ts +0 -1
- package/dist/src/helpers/withResolvers.d.ts +0 -14
- package/dist/src/rpc/retryable.d.ts +0 -23
- package/dist/src/rtc/helpers/rtcConfiguration.d.ts +0 -2
- package/src/helpers/ensureExhausted.ts +0 -5
- package/src/helpers/withResolvers.ts +0 -43
- package/src/rpc/__tests__/retryable.test.ts +0 -72
- package/src/rpc/retryable.ts +0 -57
- package/src/rtc/helpers/rtcConfiguration.ts +0 -11
package/dist/index.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
|
|
3
|
+
import { ServiceType, stackIntercept } from '@protobuf-ts/runtime-rpc';
|
|
4
4
|
import axios, { AxiosHeaders } from 'axios';
|
|
5
5
|
export { AxiosError } from 'axios';
|
|
6
|
-
import { TwirpFetchTransport
|
|
6
|
+
import { TwirpFetchTransport } from '@protobuf-ts/twirp-transport';
|
|
7
7
|
import { ReplaySubject, combineLatest, BehaviorSubject, map as map$1, shareReplay, distinctUntilChanged, takeWhile, distinctUntilKeyChanged, fromEventPattern, startWith, concatMap, merge, from, fromEvent, debounceTime, pairwise, of, filter, debounce, timer } from 'rxjs';
|
|
8
8
|
import * as SDP from 'sdp-transform';
|
|
9
9
|
import { UAParser } from 'ua-parser-js';
|
|
10
|
-
import WebSocket
|
|
10
|
+
import WebSocket from 'isomorphic-ws';
|
|
11
11
|
import { fromByteArray } from 'base64-js';
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -1014,10 +1014,6 @@ 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";
|
|
1021
1017
|
})(CallEndedReason || (CallEndedReason = {}));
|
|
1022
1018
|
/**
|
|
1023
1019
|
* WebsocketReconnectStrategy defines the ws strategies available for handling reconnections.
|
|
@@ -1031,7 +1027,7 @@ var WebsocketReconnectStrategy;
|
|
|
1031
1027
|
*/
|
|
1032
1028
|
WebsocketReconnectStrategy[WebsocketReconnectStrategy["UNSPECIFIED"] = 0] = "UNSPECIFIED";
|
|
1033
1029
|
/**
|
|
1034
|
-
* Sent after reaching the maximum reconnection attempts,
|
|
1030
|
+
* Sent after reaching the maximum reconnection attempts, leading to permanent disconnect.
|
|
1035
1031
|
*
|
|
1036
1032
|
* @generated from protobuf enum value: WEBSOCKET_RECONNECT_STRATEGY_DISCONNECT = 1;
|
|
1037
1033
|
*/
|
|
@@ -1044,18 +1040,25 @@ var WebsocketReconnectStrategy;
|
|
|
1044
1040
|
*/
|
|
1045
1041
|
WebsocketReconnectStrategy[WebsocketReconnectStrategy["FAST"] = 2] = "FAST";
|
|
1046
1042
|
/**
|
|
1047
|
-
* SDK should
|
|
1043
|
+
* SDK should drop existing pc instances and creates a fresh WebSocket connection,
|
|
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
|
|
1048
1051
|
* a completely new WebSocket connection, ensuring a comprehensive reset.
|
|
1049
1052
|
*
|
|
1050
|
-
* @generated from protobuf enum value:
|
|
1053
|
+
* @generated from protobuf enum value: WEBSOCKET_RECONNECT_STRATEGY_FULL = 4;
|
|
1051
1054
|
*/
|
|
1052
|
-
WebsocketReconnectStrategy[WebsocketReconnectStrategy["
|
|
1055
|
+
WebsocketReconnectStrategy[WebsocketReconnectStrategy["FULL"] = 4] = "FULL";
|
|
1053
1056
|
/**
|
|
1054
1057
|
* SDK should migrate to a new SFU instance
|
|
1055
1058
|
*
|
|
1056
|
-
* @generated from protobuf enum value: WEBSOCKET_RECONNECT_STRATEGY_MIGRATE =
|
|
1059
|
+
* @generated from protobuf enum value: WEBSOCKET_RECONNECT_STRATEGY_MIGRATE = 5;
|
|
1057
1060
|
*/
|
|
1058
|
-
WebsocketReconnectStrategy[WebsocketReconnectStrategy["MIGRATE"] =
|
|
1061
|
+
WebsocketReconnectStrategy[WebsocketReconnectStrategy["MIGRATE"] = 5] = "MIGRATE";
|
|
1059
1062
|
})(WebsocketReconnectStrategy || (WebsocketReconnectStrategy = {}));
|
|
1060
1063
|
// @generated message type with reflection information, may provide speed optimized methods
|
|
1061
1064
|
class CallState$Type extends MessageType {
|
|
@@ -1857,7 +1860,6 @@ class TrackInfo$Type extends MessageType {
|
|
|
1857
1860
|
{ no: 7, name: 'dtx', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
|
|
1858
1861
|
{ no: 8, name: 'stereo', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
|
|
1859
1862
|
{ no: 9, name: 'red', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
|
|
1860
|
-
{ no: 10, name: 'muted', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
|
|
1861
1863
|
]);
|
|
1862
1864
|
}
|
|
1863
1865
|
create(value) {
|
|
@@ -1869,7 +1871,6 @@ class TrackInfo$Type extends MessageType {
|
|
|
1869
1871
|
message.dtx = false;
|
|
1870
1872
|
message.stereo = false;
|
|
1871
1873
|
message.red = false;
|
|
1872
|
-
message.muted = false;
|
|
1873
1874
|
if (value !== undefined)
|
|
1874
1875
|
reflectionMergePartial(this, message, value);
|
|
1875
1876
|
return message;
|
|
@@ -1900,9 +1901,6 @@ class TrackInfo$Type extends MessageType {
|
|
|
1900
1901
|
case /* bool red */ 9:
|
|
1901
1902
|
message.red = reader.bool();
|
|
1902
1903
|
break;
|
|
1903
|
-
case /* bool muted */ 10:
|
|
1904
|
-
message.muted = reader.bool();
|
|
1905
|
-
break;
|
|
1906
1904
|
default:
|
|
1907
1905
|
let u = options.readUnknownField;
|
|
1908
1906
|
if (u === 'throw')
|
|
@@ -1936,9 +1934,6 @@ class TrackInfo$Type extends MessageType {
|
|
|
1936
1934
|
/* bool red = 9; */
|
|
1937
1935
|
if (message.red !== false)
|
|
1938
1936
|
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);
|
|
1942
1937
|
let u = options.writeUnknownFields;
|
|
1943
1938
|
if (u !== false)
|
|
1944
1939
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
|
@@ -1950,6 +1945,108 @@ class TrackInfo$Type extends MessageType {
|
|
|
1950
1945
|
*/
|
|
1951
1946
|
const TrackInfo = new TrackInfo$Type();
|
|
1952
1947
|
// @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
|
|
1953
2050
|
let Error$Type$1 = class Error$Type extends MessageType {
|
|
1954
2051
|
constructor() {
|
|
1955
2052
|
super('stream.video.sfu.models.Error', [
|
|
@@ -2343,108 +2440,6 @@ class Device$Type extends MessageType {
|
|
|
2343
2440
|
*/
|
|
2344
2441
|
const Device = new Device$Type();
|
|
2345
2442
|
// @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
|
|
2448
2443
|
class CallGrants$Type extends MessageType {
|
|
2449
2444
|
constructor() {
|
|
2450
2445
|
super('stream.video.sfu.models.CallGrants', [
|
|
@@ -3975,13 +3970,6 @@ class SfuEvent$Type extends MessageType {
|
|
|
3975
3970
|
oneof: 'eventPayload',
|
|
3976
3971
|
T: () => ParticipantUpdated,
|
|
3977
3972
|
},
|
|
3978
|
-
{
|
|
3979
|
-
no: 25,
|
|
3980
|
-
name: 'participant_migration_complete',
|
|
3981
|
-
kind: 'message',
|
|
3982
|
-
oneof: 'eventPayload',
|
|
3983
|
-
T: () => ParticipantMigrationComplete,
|
|
3984
|
-
},
|
|
3985
3973
|
]);
|
|
3986
3974
|
}
|
|
3987
3975
|
create(value) {
|
|
@@ -4116,12 +4104,6 @@ class SfuEvent$Type extends MessageType {
|
|
|
4116
4104
|
participantUpdated: ParticipantUpdated.internalBinaryRead(reader, reader.uint32(), options, message.eventPayload.participantUpdated),
|
|
4117
4105
|
};
|
|
4118
4106
|
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;
|
|
4125
4107
|
default:
|
|
4126
4108
|
let u = options.readUnknownField;
|
|
4127
4109
|
if (u === 'throw')
|
|
@@ -4194,9 +4176,6 @@ class SfuEvent$Type extends MessageType {
|
|
|
4194
4176
|
/* stream.video.sfu.event.ParticipantUpdated participant_updated = 24; */
|
|
4195
4177
|
if (message.eventPayload.oneofKind === 'participantUpdated')
|
|
4196
4178
|
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();
|
|
4200
4179
|
let u = options.writeUnknownFields;
|
|
4201
4180
|
if (u !== false)
|
|
4202
4181
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
|
@@ -4208,31 +4187,6 @@ class SfuEvent$Type extends MessageType {
|
|
|
4208
4187
|
*/
|
|
4209
4188
|
const SfuEvent = new SfuEvent$Type();
|
|
4210
4189
|
// @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
|
|
4236
4190
|
class PinsChanged$Type extends MessageType {
|
|
4237
4191
|
constructor() {
|
|
4238
4192
|
super('stream.video.sfu.event.PinsChanged', [
|
|
@@ -4483,13 +4437,6 @@ class SfuRequest$Type extends MessageType {
|
|
|
4483
4437
|
oneof: 'requestPayload',
|
|
4484
4438
|
T: () => HealthCheckRequest,
|
|
4485
4439
|
},
|
|
4486
|
-
{
|
|
4487
|
-
no: 3,
|
|
4488
|
-
name: 'leave_call_request',
|
|
4489
|
-
kind: 'message',
|
|
4490
|
-
oneof: 'requestPayload',
|
|
4491
|
-
T: () => LeaveCallRequest,
|
|
4492
|
-
},
|
|
4493
4440
|
]);
|
|
4494
4441
|
}
|
|
4495
4442
|
create(value) {
|
|
@@ -4516,12 +4463,6 @@ class SfuRequest$Type extends MessageType {
|
|
|
4516
4463
|
healthCheckRequest: HealthCheckRequest.internalBinaryRead(reader, reader.uint32(), options, message.requestPayload.healthCheckRequest),
|
|
4517
4464
|
};
|
|
4518
4465
|
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;
|
|
4525
4466
|
default:
|
|
4526
4467
|
let u = options.readUnknownField;
|
|
4527
4468
|
if (u === 'throw')
|
|
@@ -4540,9 +4481,6 @@ class SfuRequest$Type extends MessageType {
|
|
|
4540
4481
|
/* stream.video.sfu.event.HealthCheckRequest health_check_request = 2; */
|
|
4541
4482
|
if (message.requestPayload.oneofKind === 'healthCheckRequest')
|
|
4542
4483
|
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();
|
|
4546
4484
|
let u = options.writeUnknownFields;
|
|
4547
4485
|
if (u !== false)
|
|
4548
4486
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
|
@@ -4554,64 +4492,9 @@ class SfuRequest$Type extends MessageType {
|
|
|
4554
4492
|
*/
|
|
4555
4493
|
const SfuRequest = new SfuRequest$Type();
|
|
4556
4494
|
// @generated message type with reflection information, may provide speed optimized methods
|
|
4557
|
-
class
|
|
4495
|
+
class HealthCheckRequest$Type extends MessageType {
|
|
4558
4496
|
constructor() {
|
|
4559
|
-
super('stream.video.sfu.event.
|
|
4560
|
-
{ no: 1, name: 'session_id', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
|
|
4561
|
-
{ no: 2, name: 'reason', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
|
|
4562
|
-
]);
|
|
4563
|
-
}
|
|
4564
|
-
create(value) {
|
|
4565
|
-
const message = globalThis.Object.create(this.messagePrototype);
|
|
4566
|
-
message.sessionId = '';
|
|
4567
|
-
message.reason = '';
|
|
4568
|
-
if (value !== undefined)
|
|
4569
|
-
reflectionMergePartial(this, message, value);
|
|
4570
|
-
return message;
|
|
4571
|
-
}
|
|
4572
|
-
internalBinaryRead(reader, length, options, target) {
|
|
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', []);
|
|
4497
|
+
super('stream.video.sfu.event.HealthCheckRequest', []);
|
|
4615
4498
|
}
|
|
4616
4499
|
create(value) {
|
|
4617
4500
|
const message = globalThis.Object.create(this.messagePrototype);
|
|
@@ -4884,12 +4767,6 @@ class JoinRequest$Type extends MessageType {
|
|
|
4884
4767
|
kind: 'scalar',
|
|
4885
4768
|
T: 8 /*ScalarType.BOOL*/,
|
|
4886
4769
|
},
|
|
4887
|
-
{
|
|
4888
|
-
no: 7,
|
|
4889
|
-
name: 'reconnect_details',
|
|
4890
|
-
kind: 'message',
|
|
4891
|
-
T: () => ReconnectDetails,
|
|
4892
|
-
},
|
|
4893
4770
|
]);
|
|
4894
4771
|
}
|
|
4895
4772
|
create(value) {
|
|
@@ -4919,15 +4796,12 @@ class JoinRequest$Type extends MessageType {
|
|
|
4919
4796
|
case /* stream.video.sfu.models.ClientDetails client_details */ 4:
|
|
4920
4797
|
message.clientDetails = ClientDetails.internalBinaryRead(reader, reader.uint32(), options, message.clientDetails);
|
|
4921
4798
|
break;
|
|
4922
|
-
case /* stream.video.sfu.event.Migration migration
|
|
4799
|
+
case /* stream.video.sfu.event.Migration migration */ 5:
|
|
4923
4800
|
message.migration = Migration.internalBinaryRead(reader, reader.uint32(), options, message.migration);
|
|
4924
4801
|
break;
|
|
4925
|
-
case /* bool fast_reconnect
|
|
4802
|
+
case /* bool fast_reconnect */ 6:
|
|
4926
4803
|
message.fastReconnect = reader.bool();
|
|
4927
4804
|
break;
|
|
4928
|
-
case /* stream.video.sfu.event.ReconnectDetails reconnect_details */ 7:
|
|
4929
|
-
message.reconnectDetails = ReconnectDetails.internalBinaryRead(reader, reader.uint32(), options, message.reconnectDetails);
|
|
4930
|
-
break;
|
|
4931
4805
|
default:
|
|
4932
4806
|
let u = options.readUnknownField;
|
|
4933
4807
|
if (u === 'throw')
|
|
@@ -4952,15 +4826,12 @@ class JoinRequest$Type extends MessageType {
|
|
|
4952
4826
|
/* stream.video.sfu.models.ClientDetails client_details = 4; */
|
|
4953
4827
|
if (message.clientDetails)
|
|
4954
4828
|
ClientDetails.internalBinaryWrite(message.clientDetails, writer.tag(4, WireType.LengthDelimited).fork(), options).join();
|
|
4955
|
-
/* stream.video.sfu.event.Migration migration = 5
|
|
4829
|
+
/* stream.video.sfu.event.Migration migration = 5; */
|
|
4956
4830
|
if (message.migration)
|
|
4957
4831
|
Migration.internalBinaryWrite(message.migration, writer.tag(5, WireType.LengthDelimited).fork(), options).join();
|
|
4958
|
-
/* bool fast_reconnect = 6
|
|
4832
|
+
/* bool fast_reconnect = 6; */
|
|
4959
4833
|
if (message.fastReconnect !== false)
|
|
4960
4834
|
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();
|
|
4964
4835
|
let u = options.writeUnknownFields;
|
|
4965
4836
|
if (u !== false)
|
|
4966
4837
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
|
@@ -4972,129 +4843,6 @@ class JoinRequest$Type extends MessageType {
|
|
|
4972
4843
|
*/
|
|
4973
4844
|
const JoinRequest = new JoinRequest$Type();
|
|
4974
4845
|
// @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
|
|
5098
4846
|
class Migration$Type extends MessageType {
|
|
5099
4847
|
constructor() {
|
|
5100
4848
|
super('stream.video.sfu.event.Migration', [
|
|
@@ -5180,18 +4928,11 @@ class JoinResponse$Type extends MessageType {
|
|
|
5180
4928
|
super('stream.video.sfu.event.JoinResponse', [
|
|
5181
4929
|
{ no: 1, name: 'call_state', kind: 'message', T: () => CallState$1 },
|
|
5182
4930
|
{ 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
|
-
},
|
|
5189
4931
|
]);
|
|
5190
4932
|
}
|
|
5191
4933
|
create(value) {
|
|
5192
4934
|
const message = globalThis.Object.create(this.messagePrototype);
|
|
5193
4935
|
message.reconnected = false;
|
|
5194
|
-
message.fastReconnectDeadlineSeconds = 0;
|
|
5195
4936
|
if (value !== undefined)
|
|
5196
4937
|
reflectionMergePartial(this, message, value);
|
|
5197
4938
|
return message;
|
|
@@ -5207,9 +4948,6 @@ class JoinResponse$Type extends MessageType {
|
|
|
5207
4948
|
case /* bool reconnected */ 2:
|
|
5208
4949
|
message.reconnected = reader.bool();
|
|
5209
4950
|
break;
|
|
5210
|
-
case /* int32 fast_reconnect_deadline_seconds */ 3:
|
|
5211
|
-
message.fastReconnectDeadlineSeconds = reader.int32();
|
|
5212
|
-
break;
|
|
5213
4951
|
default:
|
|
5214
4952
|
let u = options.readUnknownField;
|
|
5215
4953
|
if (u === 'throw')
|
|
@@ -5228,11 +4966,6 @@ class JoinResponse$Type extends MessageType {
|
|
|
5228
4966
|
/* bool reconnected = 2; */
|
|
5229
4967
|
if (message.reconnected !== false)
|
|
5230
4968
|
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);
|
|
5236
4969
|
let u = options.writeUnknownFields;
|
|
5237
4970
|
if (u !== false)
|
|
5238
4971
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
|
@@ -6433,15 +6166,12 @@ var events = /*#__PURE__*/Object.freeze({
|
|
|
6433
6166
|
ICETrickle: ICETrickle,
|
|
6434
6167
|
JoinRequest: JoinRequest,
|
|
6435
6168
|
JoinResponse: JoinResponse,
|
|
6436
|
-
LeaveCallRequest: LeaveCallRequest,
|
|
6437
6169
|
Migration: Migration,
|
|
6438
6170
|
ParticipantJoined: ParticipantJoined,
|
|
6439
6171
|
ParticipantLeft: ParticipantLeft,
|
|
6440
|
-
ParticipantMigrationComplete: ParticipantMigrationComplete,
|
|
6441
6172
|
ParticipantUpdated: ParticipantUpdated,
|
|
6442
6173
|
PinsChanged: PinsChanged,
|
|
6443
6174
|
PublisherAnswer: PublisherAnswer,
|
|
6444
|
-
ReconnectDetails: ReconnectDetails,
|
|
6445
6175
|
SfuEvent: SfuEvent,
|
|
6446
6176
|
SfuRequest: SfuRequest,
|
|
6447
6177
|
SubscriberOffer: SubscriberOffer,
|
|
@@ -6567,17 +6297,6 @@ const withHeaders = (headers) => {
|
|
|
6567
6297
|
},
|
|
6568
6298
|
};
|
|
6569
6299
|
};
|
|
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
|
-
};
|
|
6581
6300
|
/**
|
|
6582
6301
|
* Creates new SignalServerClient instance.
|
|
6583
6302
|
*
|
|
@@ -6591,196 +6310,68 @@ const createSignalClient = (options) => {
|
|
|
6591
6310
|
return new SignalServerClient(transport);
|
|
6592
6311
|
};
|
|
6593
6312
|
|
|
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
|
-
}
|
|
6601
6313
|
/**
|
|
6602
|
-
*
|
|
6314
|
+
* Checks whether we are using React Native
|
|
6603
6315
|
*/
|
|
6604
|
-
const
|
|
6605
|
-
|
|
6606
|
-
|
|
6607
|
-
|
|
6608
|
-
WS_POLICY_VIOLATION: 1008,
|
|
6316
|
+
const isReactNative = () => {
|
|
6317
|
+
if (typeof navigator === 'undefined')
|
|
6318
|
+
return false;
|
|
6319
|
+
return navigator.product?.toLowerCase() === 'reactnative';
|
|
6609
6320
|
};
|
|
6610
|
-
|
|
6611
|
-
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
6622
|
-
|
|
6623
|
-
|
|
6624
|
-
|
|
6625
|
-
|
|
6626
|
-
|
|
6627
|
-
|
|
6628
|
-
|
|
6629
|
-
|
|
6630
|
-
|
|
6631
|
-
|
|
6632
|
-
|
|
6633
|
-
|
|
6634
|
-
|
|
6635
|
-
|
|
6636
|
-
|
|
6637
|
-
|
|
6638
|
-
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
|
|
6643
|
-
'
|
|
6644
|
-
|
|
6645
|
-
|
|
6646
|
-
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
bytes[i] = Math.random() * max;
|
|
6321
|
+
|
|
6322
|
+
// log levels, sorted by verbosity
|
|
6323
|
+
const logLevels = Object.freeze({
|
|
6324
|
+
trace: 0,
|
|
6325
|
+
debug: 1,
|
|
6326
|
+
info: 2,
|
|
6327
|
+
warn: 3,
|
|
6328
|
+
error: 4,
|
|
6329
|
+
});
|
|
6330
|
+
let logger$4;
|
|
6331
|
+
let level = 'info';
|
|
6332
|
+
const logToConsole = (logLevel, message, ...args) => {
|
|
6333
|
+
let logMethod;
|
|
6334
|
+
switch (logLevel) {
|
|
6335
|
+
case 'error':
|
|
6336
|
+
if (isReactNative()) {
|
|
6337
|
+
message = `ERROR: ${message}`;
|
|
6338
|
+
logMethod = console.info;
|
|
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;
|
|
6650
6360
|
}
|
|
6651
|
-
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
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);
|
|
6361
|
+
logMethod(message, ...args);
|
|
6362
|
+
};
|
|
6363
|
+
const setLogger = (l, lvl) => {
|
|
6364
|
+
logger$4 = l;
|
|
6365
|
+
if (lvl) {
|
|
6366
|
+
setLogLevel(lvl);
|
|
6775
6367
|
}
|
|
6776
6368
|
};
|
|
6777
6369
|
const setLogLevel = (l) => {
|
|
6778
6370
|
level = l;
|
|
6779
6371
|
};
|
|
6780
|
-
const getLogLevel = () => level;
|
|
6781
6372
|
const getLogger = (withTags) => {
|
|
6782
|
-
const loggerMethod = logger$
|
|
6783
|
-
const tags = (withTags || []).
|
|
6373
|
+
const loggerMethod = logger$4 || logToConsole;
|
|
6374
|
+
const tags = (withTags || []).join(':');
|
|
6784
6375
|
const result = (logLevel, message, ...args) => {
|
|
6785
6376
|
if (logLevels[logLevel] >= logLevels[level]) {
|
|
6786
6377
|
loggerMethod(logLevel, `[${tags}]: ${message}`, ...args);
|
|
@@ -6789,37 +6380,6 @@ const getLogger = (withTags) => {
|
|
|
6789
6380
|
return result;
|
|
6790
6381
|
};
|
|
6791
6382
|
|
|
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
|
-
|
|
6823
6383
|
const getPreferredCodecs = (kind, preferredCodec, codecToRemove) => {
|
|
6824
6384
|
const logger = getLogger(['codecs']);
|
|
6825
6385
|
if (!('getCapabilities' in RTCRtpReceiver)) {
|
|
@@ -6892,7 +6452,6 @@ const sfuEventKinds = {
|
|
|
6892
6452
|
pinsUpdated: undefined,
|
|
6893
6453
|
callEnded: undefined,
|
|
6894
6454
|
participantUpdated: undefined,
|
|
6895
|
-
participantMigrationComplete: undefined,
|
|
6896
6455
|
};
|
|
6897
6456
|
const isSfuEvent = (eventName) => {
|
|
6898
6457
|
return Object.prototype.hasOwnProperty.call(sfuEventKinds, eventName);
|
|
@@ -6901,12 +6460,12 @@ class Dispatcher {
|
|
|
6901
6460
|
constructor() {
|
|
6902
6461
|
this.logger = getLogger(['Dispatcher']);
|
|
6903
6462
|
this.subscribers = {};
|
|
6904
|
-
this.dispatch = (message
|
|
6463
|
+
this.dispatch = (message) => {
|
|
6905
6464
|
const eventKind = message.eventPayload.oneofKind;
|
|
6906
6465
|
if (!eventKind)
|
|
6907
6466
|
return;
|
|
6908
6467
|
const payload = message.eventPayload[eventKind];
|
|
6909
|
-
this.logger('debug', `Dispatching ${eventKind}
|
|
6468
|
+
this.logger('debug', `Dispatching ${eventKind}`, payload);
|
|
6910
6469
|
const listeners = this.subscribers[eventKind];
|
|
6911
6470
|
if (!listeners)
|
|
6912
6471
|
return;
|
|
@@ -6948,6 +6507,7 @@ class IceTrickleBuffer {
|
|
|
6948
6507
|
constructor() {
|
|
6949
6508
|
this.subscriberCandidates = new ReplaySubject();
|
|
6950
6509
|
this.publisherCandidates = new ReplaySubject();
|
|
6510
|
+
this.logger = getLogger(['sfu-client']);
|
|
6951
6511
|
this.push = (iceTrickle) => {
|
|
6952
6512
|
if (iceTrickle.peerType === PeerType.SUBSCRIBER) {
|
|
6953
6513
|
this.subscriberCandidates.next(iceTrickle);
|
|
@@ -6956,8 +6516,7 @@ class IceTrickleBuffer {
|
|
|
6956
6516
|
this.publisherCandidates.next(iceTrickle);
|
|
6957
6517
|
}
|
|
6958
6518
|
else {
|
|
6959
|
-
|
|
6960
|
-
logger('warn', `ICETrickle, Unknown peer type`, iceTrickle);
|
|
6519
|
+
this.logger('warn', `ICETrickle, Unknown peer type`, iceTrickle);
|
|
6961
6520
|
}
|
|
6962
6521
|
};
|
|
6963
6522
|
}
|
|
@@ -6976,6 +6535,72 @@ function getIceCandidate(candidate) {
|
|
|
6976
6535
|
}
|
|
6977
6536
|
}
|
|
6978
6537
|
|
|
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
|
+
|
|
6979
6604
|
const DEFAULT_BITRATE = 1250000;
|
|
6980
6605
|
const defaultTargetResolution = {
|
|
6981
6606
|
bitrate: DEFAULT_BITRATE,
|
|
@@ -6998,6 +6623,7 @@ const findOptimalVideoLayers = (videoTrack, targetResolution = defaultTargetReso
|
|
|
6998
6623
|
const optimalVideoLayers = [];
|
|
6999
6624
|
const settings = videoTrack.getSettings();
|
|
7000
6625
|
const { width: w = 0, height: h = 0 } = settings;
|
|
6626
|
+
const isRNIos = isReactNative() && getOSInfo()?.name.toLowerCase() === 'ios';
|
|
7001
6627
|
const maxBitrate = getComputedMaxBitrate(targetResolution, w, h);
|
|
7002
6628
|
let downscaleFactor = 1;
|
|
7003
6629
|
['f', 'h', 'q'].forEach((rid) => {
|
|
@@ -7011,7 +6637,12 @@ const findOptimalVideoLayers = (videoTrack, targetResolution = defaultTargetReso
|
|
|
7011
6637
|
height: Math.round(h / downscaleFactor),
|
|
7012
6638
|
maxBitrate: Math.round(maxBitrate / downscaleFactor) || defaultBitratePerRid[rid],
|
|
7013
6639
|
scaleResolutionDownBy: downscaleFactor,
|
|
7014
|
-
|
|
6640
|
+
// Simulcast on iOS React-Native requires all encodings to share the same framerate
|
|
6641
|
+
maxFramerate: {
|
|
6642
|
+
f: 30,
|
|
6643
|
+
h: isRNIos ? 30 : 25,
|
|
6644
|
+
q: isRNIos ? 30 : 20,
|
|
6645
|
+
}[rid],
|
|
7015
6646
|
});
|
|
7016
6647
|
downscaleFactor *= 2;
|
|
7017
6648
|
});
|
|
@@ -7086,10 +6717,6 @@ const findOptimalScreenSharingLayers = (videoTrack, preferences, defaultMaxBitra
|
|
|
7086
6717
|
];
|
|
7087
6718
|
};
|
|
7088
6719
|
|
|
7089
|
-
const ensureExhausted = (x, message) => {
|
|
7090
|
-
getLogger(['helpers'])('warn', message, x);
|
|
7091
|
-
};
|
|
7092
|
-
|
|
7093
6720
|
const trackTypeToParticipantStreamKey = (trackType) => {
|
|
7094
6721
|
switch (trackType) {
|
|
7095
6722
|
case TrackType.SCREEN_SHARE:
|
|
@@ -7103,7 +6730,8 @@ const trackTypeToParticipantStreamKey = (trackType) => {
|
|
|
7103
6730
|
case TrackType.UNSPECIFIED:
|
|
7104
6731
|
throw new Error('Track type is unspecified');
|
|
7105
6732
|
default:
|
|
7106
|
-
|
|
6733
|
+
const exhaustiveTrackTypeCheck = trackType;
|
|
6734
|
+
throw new Error(`Unknown track type: ${exhaustiveTrackTypeCheck}`);
|
|
7107
6735
|
}
|
|
7108
6736
|
};
|
|
7109
6737
|
const muteTypeToTrackType = (muteType) => {
|
|
@@ -7117,21 +6745,8 @@ const muteTypeToTrackType = (muteType) => {
|
|
|
7117
6745
|
case 'screenshare_audio':
|
|
7118
6746
|
return TrackType.SCREEN_SHARE_AUDIO;
|
|
7119
6747
|
default:
|
|
7120
|
-
|
|
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;
|
|
6748
|
+
const exhaustiveMuteTypeCheck = muteType;
|
|
6749
|
+
throw new Error(`Unknown mute type: ${exhaustiveMuteTypeCheck}`);
|
|
7135
6750
|
}
|
|
7136
6751
|
};
|
|
7137
6752
|
|
|
@@ -7651,11 +7266,6 @@ class CallState {
|
|
|
7651
7266
|
this.anonymousParticipantCountSubject = new BehaviorSubject(0);
|
|
7652
7267
|
this.participantsSubject = new BehaviorSubject([]);
|
|
7653
7268
|
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 = [];
|
|
7659
7269
|
this.logger = getLogger(['CallState']);
|
|
7660
7270
|
/**
|
|
7661
7271
|
* A list of comparators that are used to sort the participants.
|
|
@@ -7845,7 +7455,7 @@ class CallState {
|
|
|
7845
7455
|
*/
|
|
7846
7456
|
this.updateParticipants = (patch) => {
|
|
7847
7457
|
if (Object.keys(patch).length === 0)
|
|
7848
|
-
return
|
|
7458
|
+
return;
|
|
7849
7459
|
return this.setParticipants((participants) => participants.map((p) => {
|
|
7850
7460
|
const thePatch = patch[p.sessionId];
|
|
7851
7461
|
if (thePatch) {
|
|
@@ -7904,41 +7514,6 @@ class CallState {
|
|
|
7904
7514
|
return participant;
|
|
7905
7515
|
}));
|
|
7906
7516
|
};
|
|
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
|
-
};
|
|
7942
7517
|
/**
|
|
7943
7518
|
* Updates the call state with the data received from the server.
|
|
7944
7519
|
*
|
|
@@ -7963,43 +7538,6 @@ class CallState {
|
|
|
7963
7538
|
this.setCurrentValue(this.transcribingSubject, call.transcribing);
|
|
7964
7539
|
this.setCurrentValue(this.thumbnailsSubject, call.thumbnails);
|
|
7965
7540
|
};
|
|
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
|
-
};
|
|
8003
7541
|
this.updateFromMemberRemoved = (event) => {
|
|
8004
7542
|
this.updateFromCallResponse(event.call);
|
|
8005
7543
|
this.setCurrentValue(this.membersSubject, (members) => members.filter((m) => event.members.indexOf(m.user_id) === -1));
|
|
@@ -8711,75 +8249,9 @@ const enableHighQualityAudio = (sdp, trackMid, maxBitrate = 510000) => {
|
|
|
8711
8249
|
return SDP.write(parsedSdp);
|
|
8712
8250
|
};
|
|
8713
8251
|
|
|
8714
|
-
const
|
|
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
|
-
|
|
8252
|
+
const logger$3 = getLogger(['Publisher']);
|
|
8780
8253
|
/**
|
|
8781
8254
|
* The `Publisher` is responsible for publishing/unpublishing media streams to/from the SFU
|
|
8782
|
-
*
|
|
8783
8255
|
* @internal
|
|
8784
8256
|
*/
|
|
8785
8257
|
class Publisher {
|
|
@@ -8804,9 +8276,8 @@ class Publisher {
|
|
|
8804
8276
|
* @param isRedEnabled whether RED is enabled.
|
|
8805
8277
|
* @param iceRestartDelay the delay in milliseconds to wait before restarting ICE once connection goes to `disconnected` state.
|
|
8806
8278
|
* @param onUnrecoverableError a callback to call when an unrecoverable error occurs.
|
|
8807
|
-
* @param logTag the log tag to use.
|
|
8808
8279
|
*/
|
|
8809
|
-
constructor({ connectionConfig, sfuClient, dispatcher, state, isDtxEnabled, isRedEnabled,
|
|
8280
|
+
constructor({ connectionConfig, sfuClient, dispatcher, state, isDtxEnabled, isRedEnabled, iceRestartDelay = 2500, onUnrecoverableError, }) {
|
|
8810
8281
|
this.transceiverRegistry = {
|
|
8811
8282
|
[TrackType.AUDIO]: undefined,
|
|
8812
8283
|
[TrackType.VIDEO]: undefined,
|
|
@@ -8820,7 +8291,7 @@ class Publisher {
|
|
|
8820
8291
|
* This is needed because some browsers (Firefox) don't reliably report
|
|
8821
8292
|
* trackId and `mid` parameters.
|
|
8822
8293
|
*
|
|
8823
|
-
* @
|
|
8294
|
+
* @private
|
|
8824
8295
|
*/
|
|
8825
8296
|
this.transceiverInitOrder = [];
|
|
8826
8297
|
this.trackKindMapping = {
|
|
@@ -8852,7 +8323,7 @@ class Publisher {
|
|
|
8852
8323
|
/**
|
|
8853
8324
|
* Closes the publisher PeerConnection and cleans up the resources.
|
|
8854
8325
|
*/
|
|
8855
|
-
this.close = ({ stopTracks }) => {
|
|
8326
|
+
this.close = ({ stopTracks = true } = {}) => {
|
|
8856
8327
|
if (stopTracks) {
|
|
8857
8328
|
this.stopPublishing();
|
|
8858
8329
|
Object.keys(this.transceiverRegistry).forEach((trackType) => {
|
|
@@ -8864,22 +8335,10 @@ class Publisher {
|
|
|
8864
8335
|
this.trackLayersCache[trackType] = undefined;
|
|
8865
8336
|
});
|
|
8866
8337
|
}
|
|
8867
|
-
this.
|
|
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 = () => {
|
|
8338
|
+
clearTimeout(this.iceRestartTimeout);
|
|
8876
8339
|
this.unsubscribeOnIceRestart();
|
|
8877
|
-
this.pc.removeEventListener('icecandidate', this.onIceCandidate);
|
|
8878
8340
|
this.pc.removeEventListener('negotiationneeded', this.onNegotiationNeeded);
|
|
8879
|
-
this.pc.
|
|
8880
|
-
this.pc.removeEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
|
|
8881
|
-
this.pc.removeEventListener('icegatheringstatechange', this.onIceGatheringStateChange);
|
|
8882
|
-
this.pc.removeEventListener('signalingstatechange', this.onSignalingStateChange);
|
|
8341
|
+
this.pc.close();
|
|
8883
8342
|
};
|
|
8884
8343
|
/**
|
|
8885
8344
|
* Starts publishing the given track of the given media stream.
|
|
@@ -8906,7 +8365,7 @@ class Publisher {
|
|
|
8906
8365
|
* Once the track has ended, it will notify the SFU and update the state.
|
|
8907
8366
|
*/
|
|
8908
8367
|
const handleTrackEnded = async () => {
|
|
8909
|
-
|
|
8368
|
+
logger$3('info', `Track ${TrackType[trackType]} has ended, notifying the SFU`);
|
|
8910
8369
|
await this.notifyTrackMuteStateChanged(mediaStream, trackType, true);
|
|
8911
8370
|
// clean-up, this event listener needs to run only once.
|
|
8912
8371
|
track.removeEventListener('ended', handleTrackEnded);
|
|
@@ -8922,18 +8381,21 @@ class Publisher {
|
|
|
8922
8381
|
? findOptimalScreenSharingLayers(track, opts.screenShareSettings, screenShareBitrate)
|
|
8923
8382
|
: undefined;
|
|
8924
8383
|
let preferredCodec = opts.preferredCodec;
|
|
8925
|
-
if (!preferredCodec && trackType === TrackType.VIDEO
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
8384
|
+
if (!preferredCodec && trackType === TrackType.VIDEO) {
|
|
8385
|
+
if (isReactNative()) {
|
|
8386
|
+
const osName = getOSInfo()?.name.toLowerCase();
|
|
8387
|
+
if (osName === 'ipados') {
|
|
8388
|
+
// in ipads it was noticed that if vp8 codec is used
|
|
8389
|
+
// then the bytes sent is 0 in the outbound-rtp
|
|
8390
|
+
// so we are forcing h264 codec for ipads
|
|
8391
|
+
preferredCodec = 'H264';
|
|
8392
|
+
}
|
|
8393
|
+
else if (osName === 'android') {
|
|
8394
|
+
preferredCodec = 'VP8';
|
|
8395
|
+
}
|
|
8935
8396
|
}
|
|
8936
8397
|
}
|
|
8398
|
+
const codecPreferences = this.getCodecPreferences(trackType, preferredCodec);
|
|
8937
8399
|
// listen for 'ended' event on the track as it might be ended abruptly
|
|
8938
8400
|
// by an external factor as permission revokes, device disconnected, etc.
|
|
8939
8401
|
// keep in mind that `track.stop()` doesn't trigger this event.
|
|
@@ -8948,20 +8410,17 @@ class Publisher {
|
|
|
8948
8410
|
: undefined,
|
|
8949
8411
|
sendEncodings: videoEncodings,
|
|
8950
8412
|
});
|
|
8951
|
-
|
|
8413
|
+
logger$3('debug', `Added ${TrackType[trackType]} transceiver`);
|
|
8952
8414
|
this.transceiverInitOrder.push(trackType);
|
|
8953
8415
|
this.transceiverRegistry[trackType] = transceiver;
|
|
8954
8416
|
this.publishOptionsPerTrackType.set(trackType, opts);
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
: undefined;
|
|
8958
|
-
if (codecPreferences) {
|
|
8959
|
-
this.logger('info', `Setting ${TrackType[trackType]} codec preferences`, codecPreferences);
|
|
8417
|
+
if ('setCodecPreferences' in transceiver && codecPreferences) {
|
|
8418
|
+
logger$3('info', `Setting ${TrackType[trackType]} codec preferences`, codecPreferences);
|
|
8960
8419
|
try {
|
|
8961
8420
|
transceiver.setCodecPreferences(codecPreferences);
|
|
8962
8421
|
}
|
|
8963
8422
|
catch (err) {
|
|
8964
|
-
|
|
8423
|
+
logger$3('warn', `Couldn't set codec preferences`, err);
|
|
8965
8424
|
}
|
|
8966
8425
|
}
|
|
8967
8426
|
}
|
|
@@ -9010,17 +8469,31 @@ class Publisher {
|
|
|
9010
8469
|
* @param trackType the track type to check.
|
|
9011
8470
|
*/
|
|
9012
8471
|
this.isPublishing = (trackType) => {
|
|
9013
|
-
const
|
|
9014
|
-
if (
|
|
9015
|
-
|
|
9016
|
-
|
|
9017
|
-
|
|
8472
|
+
const transceiverForTrackType = this.transceiverRegistry[trackType];
|
|
8473
|
+
if (transceiverForTrackType && transceiverForTrackType.sender) {
|
|
8474
|
+
const sender = transceiverForTrackType.sender;
|
|
8475
|
+
return (!!sender.track &&
|
|
8476
|
+
sender.track.readyState === 'live' &&
|
|
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;
|
|
9018
8493
|
};
|
|
9019
8494
|
this.notifyTrackMuteStateChanged = async (mediaStream, trackType, isMuted) => {
|
|
9020
8495
|
await this.sfuClient.updateMuteState(trackType, isMuted);
|
|
9021
8496
|
const audioOrVideoOrScreenShareStream = trackTypeToParticipantStreamKey(trackType);
|
|
9022
|
-
if (!audioOrVideoOrScreenShareStream)
|
|
9023
|
-
return;
|
|
9024
8497
|
if (isMuted) {
|
|
9025
8498
|
this.state.updateParticipant(this.sfuClient.sessionId, (p) => ({
|
|
9026
8499
|
publishedTracks: p.publishedTracks.filter((t) => t !== trackType),
|
|
@@ -9042,7 +8515,7 @@ class Publisher {
|
|
|
9042
8515
|
* Stops publishing all tracks and stop all tracks.
|
|
9043
8516
|
*/
|
|
9044
8517
|
this.stopPublishing = () => {
|
|
9045
|
-
|
|
8518
|
+
logger$3('debug', 'Stopping publishing all tracks');
|
|
9046
8519
|
this.pc.getSenders().forEach((s) => {
|
|
9047
8520
|
s.track?.stop();
|
|
9048
8521
|
if (this.pc.signalingState !== 'closed') {
|
|
@@ -9051,15 +8524,15 @@ class Publisher {
|
|
|
9051
8524
|
});
|
|
9052
8525
|
};
|
|
9053
8526
|
this.updateVideoPublishQuality = async (enabledLayers) => {
|
|
9054
|
-
|
|
8527
|
+
logger$3('info', 'Update publish quality, requested layers by SFU:', enabledLayers);
|
|
9055
8528
|
const videoSender = this.transceiverRegistry[TrackType.VIDEO]?.sender;
|
|
9056
8529
|
if (!videoSender) {
|
|
9057
|
-
|
|
8530
|
+
logger$3('warn', 'Update publish quality, no video sender found.');
|
|
9058
8531
|
return;
|
|
9059
8532
|
}
|
|
9060
8533
|
const params = videoSender.getParameters();
|
|
9061
8534
|
if (params.encodings.length === 0) {
|
|
9062
|
-
|
|
8535
|
+
logger$3('warn', 'Update publish quality, No suitable video encoding quality found');
|
|
9063
8536
|
return;
|
|
9064
8537
|
}
|
|
9065
8538
|
let changed = false;
|
|
@@ -9078,18 +8551,18 @@ class Publisher {
|
|
|
9078
8551
|
if (layer !== undefined) {
|
|
9079
8552
|
if (layer.scaleResolutionDownBy >= 1 &&
|
|
9080
8553
|
layer.scaleResolutionDownBy !== enc.scaleResolutionDownBy) {
|
|
9081
|
-
|
|
8554
|
+
logger$3('debug', '[dynascale]: setting scaleResolutionDownBy from server', 'layer', layer.name, 'scale-resolution-down-by', layer.scaleResolutionDownBy);
|
|
9082
8555
|
enc.scaleResolutionDownBy = layer.scaleResolutionDownBy;
|
|
9083
8556
|
changed = true;
|
|
9084
8557
|
}
|
|
9085
8558
|
if (layer.maxBitrate > 0 && layer.maxBitrate !== enc.maxBitrate) {
|
|
9086
|
-
|
|
8559
|
+
logger$3('debug', '[dynascale] setting max-bitrate from the server', 'layer', layer.name, 'max-bitrate', layer.maxBitrate);
|
|
9087
8560
|
enc.maxBitrate = layer.maxBitrate;
|
|
9088
8561
|
changed = true;
|
|
9089
8562
|
}
|
|
9090
8563
|
if (layer.maxFramerate > 0 &&
|
|
9091
8564
|
layer.maxFramerate !== enc.maxFramerate) {
|
|
9092
|
-
|
|
8565
|
+
logger$3('debug', '[dynascale]: setting maxFramerate from server', 'layer', layer.name, 'max-framerate', layer.maxFramerate);
|
|
9093
8566
|
enc.maxFramerate = layer.maxFramerate;
|
|
9094
8567
|
changed = true;
|
|
9095
8568
|
}
|
|
@@ -9099,10 +8572,10 @@ class Publisher {
|
|
|
9099
8572
|
const activeLayers = params.encodings.filter((e) => e.active);
|
|
9100
8573
|
if (changed) {
|
|
9101
8574
|
await videoSender.setParameters(params);
|
|
9102
|
-
|
|
8575
|
+
logger$3('info', `Update publish quality, enabled rids: `, activeLayers);
|
|
9103
8576
|
}
|
|
9104
8577
|
else {
|
|
9105
|
-
|
|
8578
|
+
logger$3('info', `Update publish quality, no change: `, activeLayers);
|
|
9106
8579
|
}
|
|
9107
8580
|
};
|
|
9108
8581
|
/**
|
|
@@ -9126,7 +8599,7 @@ class Publisher {
|
|
|
9126
8599
|
this.onIceCandidate = (e) => {
|
|
9127
8600
|
const { candidate } = e;
|
|
9128
8601
|
if (!candidate) {
|
|
9129
|
-
|
|
8602
|
+
logger$3('debug', 'null ice candidate');
|
|
9130
8603
|
return;
|
|
9131
8604
|
}
|
|
9132
8605
|
this.sfuClient
|
|
@@ -9135,7 +8608,7 @@ class Publisher {
|
|
|
9135
8608
|
peerType: PeerType.PUBLISHER_UNSPECIFIED,
|
|
9136
8609
|
})
|
|
9137
8610
|
.catch((err) => {
|
|
9138
|
-
|
|
8611
|
+
logger$3('warn', `ICETrickle failed`, err);
|
|
9139
8612
|
});
|
|
9140
8613
|
};
|
|
9141
8614
|
/**
|
|
@@ -9146,23 +8619,38 @@ class Publisher {
|
|
|
9146
8619
|
this.setSfuClient = (sfuClient) => {
|
|
9147
8620
|
this.sfuClient = sfuClient;
|
|
9148
8621
|
};
|
|
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
|
+
};
|
|
9149
8640
|
/**
|
|
9150
8641
|
* Restarts the ICE connection and renegotiates with the SFU.
|
|
9151
8642
|
*/
|
|
9152
8643
|
this.restartIce = async () => {
|
|
9153
|
-
|
|
8644
|
+
logger$3('debug', 'Restarting ICE connection');
|
|
9154
8645
|
const signalingState = this.pc.signalingState;
|
|
9155
8646
|
if (this.isIceRestarting || signalingState === 'have-local-offer') {
|
|
9156
|
-
|
|
8647
|
+
logger$3('debug', 'ICE restart is already in progress');
|
|
9157
8648
|
return;
|
|
9158
8649
|
}
|
|
9159
8650
|
await this.negotiate({ iceRestart: true });
|
|
9160
8651
|
};
|
|
9161
8652
|
this.onNegotiationNeeded = () => {
|
|
9162
|
-
this.negotiate().catch((err) =>
|
|
9163
|
-
this.logger('warn', `Negotiation failed.`, err);
|
|
9164
|
-
this.onUnrecoverableError?.();
|
|
9165
|
-
});
|
|
8653
|
+
this.negotiate().catch((err) => logger$3('warn', `Negotiation failed.`, err));
|
|
9166
8654
|
};
|
|
9167
8655
|
/**
|
|
9168
8656
|
* Initiates a new offer/answer exchange with the currently connected SFU.
|
|
@@ -9174,62 +8662,59 @@ class Publisher {
|
|
|
9174
8662
|
const offer = await this.pc.createOffer(options);
|
|
9175
8663
|
let sdp = this.mungeCodecs(offer.sdp);
|
|
9176
8664
|
if (sdp && this.isPublishing(TrackType.SCREEN_SHARE_AUDIO)) {
|
|
9177
|
-
|
|
8665
|
+
const transceiver = this.transceiverRegistry[TrackType.SCREEN_SHARE_AUDIO];
|
|
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
|
+
}
|
|
9178
8671
|
}
|
|
9179
8672
|
// set the munged SDP back to the offer
|
|
9180
8673
|
offer.sdp = sdp;
|
|
9181
|
-
const trackInfos = this.
|
|
8674
|
+
const trackInfos = this.getCurrentTrackInfos(offer.sdp);
|
|
9182
8675
|
if (trackInfos.length === 0) {
|
|
9183
|
-
throw new Error(`Can't
|
|
8676
|
+
throw new Error(`Can't initiate negotiation without announcing any tracks`);
|
|
9184
8677
|
}
|
|
9185
8678
|
await this.pc.setLocalDescription(offer);
|
|
9186
8679
|
const { response } = await this.sfuClient.setPublisher({
|
|
9187
8680
|
sdp: offer.sdp || '',
|
|
9188
8681
|
tracks: trackInfos,
|
|
9189
8682
|
});
|
|
9190
|
-
const { sdp: remoteSdp, error } = response;
|
|
9191
8683
|
try {
|
|
9192
|
-
await this.pc.setRemoteDescription({
|
|
8684
|
+
await this.pc.setRemoteDescription({
|
|
8685
|
+
type: 'answer',
|
|
8686
|
+
sdp: response.sdp,
|
|
8687
|
+
});
|
|
9193
8688
|
}
|
|
9194
8689
|
catch (e) {
|
|
9195
|
-
|
|
9196
|
-
|
|
9197
|
-
|
|
9198
|
-
|
|
9199
|
-
this.isIceRestarting = false;
|
|
8690
|
+
logger$3('error', `setRemoteDescription error`, {
|
|
8691
|
+
sdp: response.sdp,
|
|
8692
|
+
error: e,
|
|
8693
|
+
});
|
|
9200
8694
|
}
|
|
8695
|
+
this.isIceRestarting = false;
|
|
9201
8696
|
this.sfuClient.iceTrickleBuffer.publisherCandidates.subscribe(async (candidate) => {
|
|
9202
8697
|
try {
|
|
9203
8698
|
const iceCandidate = JSON.parse(candidate.iceCandidate);
|
|
9204
8699
|
await this.pc.addIceCandidate(iceCandidate);
|
|
9205
8700
|
}
|
|
9206
8701
|
catch (e) {
|
|
9207
|
-
|
|
8702
|
+
logger$3('warn', `ICE candidate error`, [e, candidate]);
|
|
9208
8703
|
}
|
|
9209
8704
|
});
|
|
9210
8705
|
};
|
|
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
|
-
};
|
|
9218
8706
|
this.mungeCodecs = (sdp) => {
|
|
9219
8707
|
if (sdp) {
|
|
9220
8708
|
sdp = toggleDtx(sdp, this.isDtxEnabled);
|
|
9221
8709
|
}
|
|
9222
8710
|
return sdp;
|
|
9223
8711
|
};
|
|
9224
|
-
this.extractMid = (
|
|
9225
|
-
if (transceiver.mid)
|
|
9226
|
-
return transceiver.mid;
|
|
8712
|
+
this.extractMid = (sdp, track, trackType) => {
|
|
9227
8713
|
if (!sdp) {
|
|
9228
|
-
|
|
8714
|
+
logger$3('warn', 'No SDP found. Returning empty mid');
|
|
9229
8715
|
return '';
|
|
9230
8716
|
}
|
|
9231
|
-
|
|
9232
|
-
const track = transceiver.sender.track;
|
|
8717
|
+
logger$3('debug', `No 'mid' found for track. Trying to find it from the Offer SDP`);
|
|
9233
8718
|
const parsedSdp = SDP.parse(sdp);
|
|
9234
8719
|
const media = parsedSdp.media.find((m) => {
|
|
9235
8720
|
return (m.type === track.kind &&
|
|
@@ -9237,23 +8722,17 @@ class Publisher {
|
|
|
9237
8722
|
(m.msid?.includes(track.id) ?? true));
|
|
9238
8723
|
});
|
|
9239
8724
|
if (typeof media?.mid === 'undefined') {
|
|
9240
|
-
|
|
8725
|
+
logger$3('debug', `No mid found in SDP for track type ${track.kind} and id ${track.id}. Attempting to find a heuristic mid`);
|
|
9241
8726
|
const heuristicMid = this.transceiverInitOrder.indexOf(trackType);
|
|
9242
8727
|
if (heuristicMid !== -1) {
|
|
9243
8728
|
return String(heuristicMid);
|
|
9244
8729
|
}
|
|
9245
|
-
|
|
8730
|
+
logger$3('debug', 'No heuristic mid found. Returning empty mid');
|
|
9246
8731
|
return '';
|
|
9247
8732
|
}
|
|
9248
8733
|
return String(media.mid);
|
|
9249
8734
|
};
|
|
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) => {
|
|
8735
|
+
this.getCurrentTrackInfos = (sdp) => {
|
|
9257
8736
|
sdp = sdp || this.pc.localDescription?.sdp;
|
|
9258
8737
|
const { settings } = this.state;
|
|
9259
8738
|
const targetResolution = settings?.video
|
|
@@ -9265,8 +8744,7 @@ class Publisher {
|
|
|
9265
8744
|
const trackType = Number(Object.keys(this.transceiverRegistry).find((key) => this.transceiverRegistry[key] === transceiver));
|
|
9266
8745
|
const track = transceiver.sender.track;
|
|
9267
8746
|
let optimalLayers;
|
|
9268
|
-
|
|
9269
|
-
if (isTrackLive) {
|
|
8747
|
+
if (track.readyState === 'live') {
|
|
9270
8748
|
const publishOpts = this.publishOptionsPerTrackType.get(trackType);
|
|
9271
8749
|
optimalLayers =
|
|
9272
8750
|
trackType === TrackType.VIDEO
|
|
@@ -9279,7 +8757,7 @@ class Publisher {
|
|
|
9279
8757
|
else {
|
|
9280
8758
|
// we report the last known optimal layers for ended tracks
|
|
9281
8759
|
optimalLayers = this.trackLayersCache[trackType] || [];
|
|
9282
|
-
|
|
8760
|
+
logger$3('debug', `Track ${TrackType[trackType]} is ended. Announcing last known optimal layers`, optimalLayers);
|
|
9283
8761
|
}
|
|
9284
8762
|
const layers = optimalLayers.map((optimalLayer) => ({
|
|
9285
8763
|
rid: optimalLayer.rid || '',
|
|
@@ -9301,11 +8779,10 @@ class Publisher {
|
|
|
9301
8779
|
trackId: track.id,
|
|
9302
8780
|
layers: layers,
|
|
9303
8781
|
trackType,
|
|
9304
|
-
mid: this.extractMid(
|
|
8782
|
+
mid: transceiver.mid ?? this.extractMid(sdp, track, trackType),
|
|
9305
8783
|
stereo: isStereo,
|
|
9306
8784
|
dtx: isAudioTrack && this.isDtxEnabled,
|
|
9307
8785
|
red: isAudioTrack && this.isRedEnabled,
|
|
9308
|
-
muted: !isTrackLive,
|
|
9309
8786
|
};
|
|
9310
8787
|
});
|
|
9311
8788
|
};
|
|
@@ -9314,26 +8791,44 @@ class Publisher {
|
|
|
9314
8791
|
`${e.errorCode}: ${e.errorText}`;
|
|
9315
8792
|
const iceState = this.pc.iceConnectionState;
|
|
9316
8793
|
const logLevel = iceState === 'connected' || iceState === 'checking' ? 'debug' : 'warn';
|
|
9317
|
-
|
|
8794
|
+
logger$3(logLevel, `ICE Candidate error`, errorMessage);
|
|
9318
8795
|
};
|
|
9319
8796
|
this.onIceConnectionStateChange = () => {
|
|
9320
8797
|
const state = this.pc.iceConnectionState;
|
|
9321
|
-
|
|
9322
|
-
|
|
9323
|
-
|
|
9324
|
-
|
|
9325
|
-
this.logger('debug', `Attempting to restart ICE`);
|
|
8798
|
+
logger$3('debug', `ICE Connection state changed to`, state);
|
|
8799
|
+
const hasNetworkConnection = this.state.callingState !== CallingState.OFFLINE;
|
|
8800
|
+
if (state === 'failed') {
|
|
8801
|
+
logger$3('debug', `Attempting to restart ICE`);
|
|
9326
8802
|
this.restartIce().catch((e) => {
|
|
9327
|
-
|
|
8803
|
+
logger$3('error', `ICE restart error`, e);
|
|
9328
8804
|
this.onUnrecoverableError?.();
|
|
9329
8805
|
});
|
|
9330
8806
|
}
|
|
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
|
+
}
|
|
9331
8826
|
};
|
|
9332
8827
|
this.onIceGatheringStateChange = () => {
|
|
9333
|
-
|
|
8828
|
+
logger$3('debug', `ICE Gathering State`, this.pc.iceGatheringState);
|
|
9334
8829
|
};
|
|
9335
8830
|
this.onSignalingStateChange = () => {
|
|
9336
|
-
|
|
8831
|
+
logger$3('debug', `Signaling state changed`, this.pc.signalingState);
|
|
9337
8832
|
};
|
|
9338
8833
|
this.ridToVideoQuality = (rid) => {
|
|
9339
8834
|
return rid === 'q'
|
|
@@ -9342,29 +8837,28 @@ class Publisher {
|
|
|
9342
8837
|
? VideoQuality.MID
|
|
9343
8838
|
: VideoQuality.HIGH; // default to HIGH
|
|
9344
8839
|
};
|
|
9345
|
-
this.logger = getLogger(['Publisher', logTag]);
|
|
9346
8840
|
this.pc = this.createPeerConnection(connectionConfig);
|
|
9347
8841
|
this.sfuClient = sfuClient;
|
|
9348
8842
|
this.state = state;
|
|
9349
8843
|
this.isDtxEnabled = isDtxEnabled;
|
|
9350
8844
|
this.isRedEnabled = isRedEnabled;
|
|
8845
|
+
this.iceRestartDelay = iceRestartDelay;
|
|
9351
8846
|
this.onUnrecoverableError = onUnrecoverableError;
|
|
9352
8847
|
this.unsubscribeOnIceRestart = dispatcher.on('iceRestart', (iceRestart) => {
|
|
9353
8848
|
if (iceRestart.peerType !== PeerType.PUBLISHER_UNSPECIFIED)
|
|
9354
8849
|
return;
|
|
9355
8850
|
this.restartIce().catch((err) => {
|
|
9356
|
-
|
|
8851
|
+
logger$3('warn', `ICERestart failed`, err);
|
|
9357
8852
|
this.onUnrecoverableError?.();
|
|
9358
8853
|
});
|
|
9359
8854
|
});
|
|
9360
8855
|
}
|
|
9361
8856
|
}
|
|
9362
8857
|
|
|
8858
|
+
const logger$2 = getLogger(['Subscriber']);
|
|
9363
8859
|
/**
|
|
9364
8860
|
* A wrapper around the `RTCPeerConnection` that handles the incoming
|
|
9365
8861
|
* media streams from the SFU.
|
|
9366
|
-
*
|
|
9367
|
-
* @internal
|
|
9368
8862
|
*/
|
|
9369
8863
|
class Subscriber {
|
|
9370
8864
|
/**
|
|
@@ -9386,9 +8880,8 @@ class Subscriber {
|
|
|
9386
8880
|
* @param connectionConfig the connection configuration to use.
|
|
9387
8881
|
* @param iceRestartDelay the delay in milliseconds to wait before restarting ICE when connection goes to `disconnected` state.
|
|
9388
8882
|
* @param onUnrecoverableError a callback to call when an unrecoverable error occurs.
|
|
9389
|
-
* @param logTag a tag to use for logging.
|
|
9390
8883
|
*/
|
|
9391
|
-
constructor({ sfuClient, dispatcher, state, connectionConfig,
|
|
8884
|
+
constructor({ sfuClient, dispatcher, state, connectionConfig, iceRestartDelay = 2500, onUnrecoverableError, }) {
|
|
9392
8885
|
this.isIceRestarting = false;
|
|
9393
8886
|
/**
|
|
9394
8887
|
* Creates a new `RTCPeerConnection` instance with the given configuration.
|
|
@@ -9409,22 +8902,10 @@ class Subscriber {
|
|
|
9409
8902
|
* Closes the `RTCPeerConnection` and unsubscribes from the dispatcher.
|
|
9410
8903
|
*/
|
|
9411
8904
|
this.close = () => {
|
|
9412
|
-
this.
|
|
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 = () => {
|
|
8905
|
+
clearTimeout(this.iceRestartTimeout);
|
|
9421
8906
|
this.unregisterOnSubscriberOffer();
|
|
9422
8907
|
this.unregisterOnIceRestart();
|
|
9423
|
-
this.pc.
|
|
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);
|
|
8908
|
+
this.pc.close();
|
|
9428
8909
|
};
|
|
9429
8910
|
/**
|
|
9430
8911
|
* Returns the result of the `RTCPeerConnection.getStats()` method
|
|
@@ -9442,17 +8923,76 @@ class Subscriber {
|
|
|
9442
8923
|
this.setSfuClient = (sfuClient) => {
|
|
9443
8924
|
this.sfuClient = sfuClient;
|
|
9444
8925
|
};
|
|
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
|
+
};
|
|
9445
8985
|
/**
|
|
9446
8986
|
* Restarts the ICE connection and renegotiates with the SFU.
|
|
9447
8987
|
*/
|
|
9448
8988
|
this.restartIce = async () => {
|
|
9449
|
-
|
|
8989
|
+
logger$2('debug', 'Restarting ICE connection');
|
|
9450
8990
|
if (this.pc.signalingState === 'have-remote-offer') {
|
|
9451
|
-
|
|
8991
|
+
logger$2('debug', 'ICE restart is already in progress');
|
|
9452
8992
|
return;
|
|
9453
8993
|
}
|
|
9454
8994
|
if (this.pc.connectionState === 'new') {
|
|
9455
|
-
|
|
8995
|
+
logger$2('debug', `ICE connection is not yet established, skipping restart.`);
|
|
9456
8996
|
return;
|
|
9457
8997
|
}
|
|
9458
8998
|
const previousIsIceRestarting = this.isIceRestarting;
|
|
@@ -9471,42 +9011,36 @@ class Subscriber {
|
|
|
9471
9011
|
this.handleOnTrack = (e) => {
|
|
9472
9012
|
const [primaryStream] = e.streams;
|
|
9473
9013
|
// example: `e3f6aaf8-b03d-4911-be36-83f47d37a76a:TRACK_TYPE_VIDEO`
|
|
9474
|
-
const [trackId,
|
|
9014
|
+
const [trackId, trackType] = primaryStream.id.split(':');
|
|
9475
9015
|
const participantToUpdate = this.state.participants.find((p) => p.trackLookupPrefix === trackId);
|
|
9476
|
-
|
|
9477
|
-
|
|
9016
|
+
logger$2('debug', `[onTrack]: Got remote ${trackType} track for userId: ${participantToUpdate?.userId}`, e.track.id, e.track);
|
|
9017
|
+
if (!participantToUpdate) {
|
|
9018
|
+
logger$2('warn', `[onTrack]: Received track for unknown participant: ${trackId}`, e);
|
|
9019
|
+
return;
|
|
9020
|
+
}
|
|
9021
|
+
const trackDebugInfo = `${participantToUpdate.userId} ${trackType}:${trackId}`;
|
|
9478
9022
|
e.track.addEventListener('mute', () => {
|
|
9479
|
-
|
|
9023
|
+
logger$2('info', `[onTrack]: Track muted: ${trackDebugInfo}`);
|
|
9480
9024
|
});
|
|
9481
9025
|
e.track.addEventListener('unmute', () => {
|
|
9482
|
-
|
|
9026
|
+
logger$2('info', `[onTrack]: Track unmuted: ${trackDebugInfo}`);
|
|
9483
9027
|
});
|
|
9484
9028
|
e.track.addEventListener('ended', () => {
|
|
9485
|
-
|
|
9486
|
-
this.state.removeOrphanedTrack(primaryStream.id);
|
|
9029
|
+
logger$2('info', `[onTrack]: Track ended: ${trackDebugInfo}`);
|
|
9487
9030
|
});
|
|
9488
|
-
const
|
|
9489
|
-
|
|
9490
|
-
|
|
9491
|
-
|
|
9492
|
-
|
|
9493
|
-
|
|
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);
|
|
9031
|
+
const streamKindProp = {
|
|
9032
|
+
TRACK_TYPE_AUDIO: 'audioStream',
|
|
9033
|
+
TRACK_TYPE_VIDEO: 'videoStream',
|
|
9034
|
+
TRACK_TYPE_SCREEN_SHARE: 'screenShareStream',
|
|
9035
|
+
TRACK_TYPE_SCREEN_SHARE_AUDIO: 'screenShareAudioStream',
|
|
9036
|
+
}[trackType];
|
|
9503
9037
|
if (!streamKindProp) {
|
|
9504
|
-
|
|
9038
|
+
logger$2('error', `Unknown track type: ${trackType}`);
|
|
9505
9039
|
return;
|
|
9506
9040
|
}
|
|
9507
9041
|
const previousStream = participantToUpdate[streamKindProp];
|
|
9508
9042
|
if (previousStream) {
|
|
9509
|
-
|
|
9043
|
+
logger$2('info', `[onTrack]: Cleaning up previous remote ${e.track.kind} tracks for userId: ${participantToUpdate.userId}`);
|
|
9510
9044
|
previousStream.getTracks().forEach((t) => {
|
|
9511
9045
|
t.stop();
|
|
9512
9046
|
previousStream.removeTrack(t);
|
|
@@ -9519,7 +9053,7 @@ class Subscriber {
|
|
|
9519
9053
|
this.onIceCandidate = (e) => {
|
|
9520
9054
|
const { candidate } = e;
|
|
9521
9055
|
if (!candidate) {
|
|
9522
|
-
|
|
9056
|
+
logger$2('debug', 'null ice candidate');
|
|
9523
9057
|
return;
|
|
9524
9058
|
}
|
|
9525
9059
|
this.sfuClient
|
|
@@ -9528,11 +9062,11 @@ class Subscriber {
|
|
|
9528
9062
|
peerType: PeerType.SUBSCRIBER,
|
|
9529
9063
|
})
|
|
9530
9064
|
.catch((err) => {
|
|
9531
|
-
|
|
9065
|
+
logger$2('warn', `ICETrickle failed`, err);
|
|
9532
9066
|
});
|
|
9533
9067
|
};
|
|
9534
9068
|
this.negotiate = async (subscriberOffer) => {
|
|
9535
|
-
|
|
9069
|
+
logger$2('info', `Received subscriberOffer`, subscriberOffer);
|
|
9536
9070
|
await this.pc.setRemoteDescription({
|
|
9537
9071
|
type: 'offer',
|
|
9538
9072
|
sdp: subscriberOffer.sdp,
|
|
@@ -9543,7 +9077,7 @@ class Subscriber {
|
|
|
9543
9077
|
await this.pc.addIceCandidate(iceCandidate);
|
|
9544
9078
|
}
|
|
9545
9079
|
catch (e) {
|
|
9546
|
-
|
|
9080
|
+
logger$2('warn', `ICE candidate error`, [e, candidate]);
|
|
9547
9081
|
}
|
|
9548
9082
|
});
|
|
9549
9083
|
const answer = await this.pc.createAnswer();
|
|
@@ -9556,53 +9090,63 @@ class Subscriber {
|
|
|
9556
9090
|
};
|
|
9557
9091
|
this.onIceConnectionStateChange = () => {
|
|
9558
9092
|
const state = this.pc.iceConnectionState;
|
|
9559
|
-
|
|
9560
|
-
if (this.state.callingState === CallingState.RECONNECTING)
|
|
9561
|
-
return;
|
|
9093
|
+
logger$2('debug', `ICE connection state changed`, state);
|
|
9562
9094
|
// do nothing when ICE is restarting
|
|
9563
9095
|
if (this.isIceRestarting)
|
|
9564
9096
|
return;
|
|
9565
|
-
|
|
9566
|
-
|
|
9097
|
+
const hasNetworkConnection = this.state.callingState !== CallingState.OFFLINE;
|
|
9098
|
+
if (state === 'failed') {
|
|
9099
|
+
logger$2('debug', `Attempting to restart ICE`);
|
|
9567
9100
|
this.restartIce().catch((e) => {
|
|
9568
|
-
|
|
9101
|
+
logger$2('error', `ICE restart failed`, e);
|
|
9569
9102
|
this.onUnrecoverableError?.();
|
|
9570
9103
|
});
|
|
9571
9104
|
}
|
|
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
|
+
}
|
|
9572
9124
|
};
|
|
9573
9125
|
this.onIceGatheringStateChange = () => {
|
|
9574
|
-
|
|
9126
|
+
logger$2('debug', `ICE gathering state changed`, this.pc.iceGatheringState);
|
|
9575
9127
|
};
|
|
9576
9128
|
this.onIceCandidateError = (e) => {
|
|
9577
9129
|
const errorMessage = e instanceof RTCPeerConnectionIceErrorEvent &&
|
|
9578
9130
|
`${e.errorCode}: ${e.errorText}`;
|
|
9579
9131
|
const iceState = this.pc.iceConnectionState;
|
|
9580
9132
|
const logLevel = iceState === 'connected' || iceState === 'checking' ? 'debug' : 'warn';
|
|
9581
|
-
|
|
9133
|
+
logger$2(logLevel, `ICE Candidate error`, errorMessage);
|
|
9582
9134
|
};
|
|
9583
|
-
this.logger = getLogger(['Subscriber', logTag]);
|
|
9584
9135
|
this.sfuClient = sfuClient;
|
|
9585
9136
|
this.state = state;
|
|
9137
|
+
this.iceRestartDelay = iceRestartDelay;
|
|
9586
9138
|
this.onUnrecoverableError = onUnrecoverableError;
|
|
9587
9139
|
this.pc = this.createPeerConnection(connectionConfig);
|
|
9588
|
-
const subscriberOfferConcurrencyTag = Symbol('subscriberOffer');
|
|
9589
9140
|
this.unregisterOnSubscriberOffer = dispatcher.on('subscriberOffer', (subscriberOffer) => {
|
|
9590
|
-
|
|
9591
|
-
|
|
9592
|
-
withoutConcurrency(subscriberOfferConcurrencyTag, () => {
|
|
9593
|
-
return this.negotiate(subscriberOffer);
|
|
9594
|
-
}).catch((err) => {
|
|
9595
|
-
this.logger('warn', `Negotiation failed.`, err);
|
|
9141
|
+
this.negotiate(subscriberOffer).catch((err) => {
|
|
9142
|
+
logger$2('warn', `Negotiation failed.`, err);
|
|
9596
9143
|
});
|
|
9597
9144
|
});
|
|
9598
|
-
const iceRestartConcurrencyTag = Symbol('iceRestart');
|
|
9599
9145
|
this.unregisterOnIceRestart = dispatcher.on('iceRestart', (iceRestart) => {
|
|
9600
|
-
|
|
9601
|
-
|
|
9602
|
-
|
|
9603
|
-
|
|
9604
|
-
}).catch((err) => {
|
|
9605
|
-
this.logger('warn', `ICERestart failed`, err);
|
|
9146
|
+
if (iceRestart.peerType !== PeerType.SUBSCRIBER)
|
|
9147
|
+
return;
|
|
9148
|
+
this.restartIce().catch((err) => {
|
|
9149
|
+
logger$2('warn', `ICERestart failed`, err);
|
|
9606
9150
|
this.onUnrecoverableError?.();
|
|
9607
9151
|
});
|
|
9608
9152
|
});
|
|
@@ -9610,8 +9154,8 @@ class Subscriber {
|
|
|
9610
9154
|
}
|
|
9611
9155
|
|
|
9612
9156
|
const createWebSocketSignalChannel = (opts) => {
|
|
9613
|
-
const
|
|
9614
|
-
const
|
|
9157
|
+
const logger = getLogger(['sfu-client']);
|
|
9158
|
+
const { endpoint, onMessage } = opts;
|
|
9615
9159
|
const ws = new WebSocket(endpoint);
|
|
9616
9160
|
ws.binaryType = 'arraybuffer'; // do we need this?
|
|
9617
9161
|
ws.addEventListener('error', (e) => {
|
|
@@ -9637,37 +9181,132 @@ const createWebSocketSignalChannel = (opts) => {
|
|
|
9637
9181
|
return ws;
|
|
9638
9182
|
};
|
|
9639
9183
|
|
|
9184
|
+
const sleep = (m) => new Promise((r) => setTimeout(r, m));
|
|
9185
|
+
function isFunction(value) {
|
|
9186
|
+
return (value &&
|
|
9187
|
+
(Object.prototype.toString.call(value) === '[object Function]' ||
|
|
9188
|
+
'function' === typeof value ||
|
|
9189
|
+
value instanceof Function));
|
|
9190
|
+
}
|
|
9640
9191
|
/**
|
|
9641
|
-
*
|
|
9642
|
-
*
|
|
9643
|
-
* Based on:
|
|
9644
|
-
* - https://github.com/tc39/proposal-promise-with-resolvers/blob/main/polyfills.js
|
|
9192
|
+
* A map of known error codes.
|
|
9645
9193
|
*/
|
|
9646
|
-
const
|
|
9647
|
-
|
|
9648
|
-
|
|
9649
|
-
|
|
9650
|
-
|
|
9651
|
-
reject = _reject;
|
|
9652
|
-
});
|
|
9653
|
-
let isResolved = false;
|
|
9654
|
-
let isRejected = false;
|
|
9655
|
-
const resolver = (value) => {
|
|
9656
|
-
isResolved = true;
|
|
9657
|
-
resolve(value);
|
|
9658
|
-
};
|
|
9659
|
-
const rejecter = (reason) => {
|
|
9660
|
-
isRejected = true;
|
|
9661
|
-
reject(reason);
|
|
9662
|
-
};
|
|
9663
|
-
return {
|
|
9664
|
-
promise,
|
|
9665
|
-
resolve: resolver,
|
|
9666
|
-
reject: rejecter,
|
|
9667
|
-
isResolved,
|
|
9668
|
-
isRejected,
|
|
9669
|
-
};
|
|
9194
|
+
const KnownCodes = {
|
|
9195
|
+
TOKEN_EXPIRED: 40,
|
|
9196
|
+
WS_CLOSED_SUCCESS: 1000,
|
|
9197
|
+
WS_CLOSED_ABRUPTLY: 1006,
|
|
9198
|
+
WS_POLICY_VIOLATION: 1008,
|
|
9670
9199
|
};
|
|
9200
|
+
/**
|
|
9201
|
+
* retryInterval - A retry interval which increases acc to number of failures
|
|
9202
|
+
*
|
|
9203
|
+
* @return {number} Duration to wait in milliseconds
|
|
9204
|
+
*/
|
|
9205
|
+
function retryInterval(numberOfFailures) {
|
|
9206
|
+
// try to reconnect in 0.25-5 seconds (random to spread out the load from failures)
|
|
9207
|
+
const max = Math.min(500 + numberOfFailures * 2000, 5000);
|
|
9208
|
+
const min = Math.min(Math.max(250, (numberOfFailures - 1) * 2000), 5000);
|
|
9209
|
+
return Math.floor(Math.random() * (max - min) + min);
|
|
9210
|
+
}
|
|
9211
|
+
function randomId() {
|
|
9212
|
+
return generateUUIDv4();
|
|
9213
|
+
}
|
|
9214
|
+
function hex(bytes) {
|
|
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
|
+
}
|
|
9671
9310
|
|
|
9672
9311
|
/**
|
|
9673
9312
|
* The client used for exchanging information with the SFU.
|
|
@@ -9675,228 +9314,133 @@ const promiseWithResolvers = () => {
|
|
|
9675
9314
|
class StreamSfuClient {
|
|
9676
9315
|
/**
|
|
9677
9316
|
* 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.
|
|
9678
9322
|
*/
|
|
9679
|
-
constructor({ dispatcher,
|
|
9323
|
+
constructor({ dispatcher, sfuServer, token, sessionId, }) {
|
|
9680
9324
|
/**
|
|
9681
9325
|
* A buffer for ICE Candidates that are received before
|
|
9682
|
-
* the
|
|
9326
|
+
* the PeerConnections are ready to handle them.
|
|
9683
9327
|
*/
|
|
9684
9328
|
this.iceTrickleBuffer = new IceTrickleBuffer();
|
|
9685
9329
|
/**
|
|
9686
|
-
*
|
|
9687
|
-
*
|
|
9688
|
-
*/
|
|
9689
|
-
this.isLeaving = false;
|
|
9690
|
-
this.pingIntervalInMs = 10 * 1000;
|
|
9691
|
-
this.unhealthyTimeoutInMs = this.pingIntervalInMs + 5 * 1000;
|
|
9692
|
-
this.restoreWebSocketConcurrencyTag = Symbol('recoverWebSocket');
|
|
9693
|
-
/**
|
|
9694
|
-
* Promise that resolves when the JoinResponse is received.
|
|
9695
|
-
* Rejects after a certain threshold if the response is not received.
|
|
9330
|
+
* A flag indicating whether the client is currently migrating away
|
|
9331
|
+
* from this SFU.
|
|
9696
9332
|
*/
|
|
9697
|
-
this.
|
|
9333
|
+
this.isMigratingAway = false;
|
|
9698
9334
|
/**
|
|
9699
|
-
* A
|
|
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.
|
|
9700
9337
|
*/
|
|
9701
|
-
this.
|
|
9702
|
-
this.
|
|
9703
|
-
|
|
9704
|
-
|
|
9705
|
-
|
|
9706
|
-
|
|
9707
|
-
this.lastMessageTimestamp = new Date();
|
|
9708
|
-
this.scheduleConnectionCheck();
|
|
9709
|
-
this.dispatcher.dispatch(message, this.logTag);
|
|
9710
|
-
},
|
|
9711
|
-
});
|
|
9712
|
-
this.signalWs.addEventListener('close', this.handleWebSocketClose);
|
|
9713
|
-
this.signalWs.addEventListener('error', this.restoreWebSocket);
|
|
9714
|
-
this.signalReady = new Promise((resolve) => {
|
|
9715
|
-
const onOpen = () => {
|
|
9716
|
-
this.signalWs.removeEventListener('open', onOpen);
|
|
9717
|
-
resolve(this.signalWs);
|
|
9718
|
-
};
|
|
9719
|
-
this.signalWs.addEventListener('open', onOpen);
|
|
9720
|
-
});
|
|
9721
|
-
};
|
|
9722
|
-
this.cleanUpWebSocket = () => {
|
|
9723
|
-
this.signalWs.removeEventListener('error', this.restoreWebSocket);
|
|
9724
|
-
this.signalWs.removeEventListener('close', this.handleWebSocketClose);
|
|
9725
|
-
};
|
|
9726
|
-
this.restoreWebSocket = () => {
|
|
9727
|
-
withoutConcurrency(this.restoreWebSocketConcurrencyTag, async () => {
|
|
9728
|
-
this.logger('debug', 'Restoring SFU WS connection');
|
|
9729
|
-
this.cleanUpWebSocket();
|
|
9730
|
-
await sleep(500);
|
|
9731
|
-
this.createWebSocket();
|
|
9732
|
-
}).catch((err) => this.logger('debug', `Can't restore WS connection`, err));
|
|
9733
|
-
};
|
|
9734
|
-
this.handleWebSocketClose = (e) => {
|
|
9735
|
-
this.signalWs.removeEventListener('close', this.handleWebSocketClose);
|
|
9736
|
-
clearInterval(this.keepAliveInterval);
|
|
9737
|
-
clearTimeout(this.connectionCheckTimeout);
|
|
9738
|
-
if (this.onSignalClose) {
|
|
9739
|
-
this.onSignalClose(e);
|
|
9740
|
-
}
|
|
9741
|
-
};
|
|
9742
|
-
this.close = (code = StreamSfuClient.NORMAL_CLOSURE, reason) => {
|
|
9743
|
-
if (this.signalWs.readyState === WebSocket.OPEN) {
|
|
9744
|
-
this.logger('debug', `Closing SFU WS connection: ${code} - ${reason}`);
|
|
9338
|
+
this.isFastReconnecting = false;
|
|
9339
|
+
this.pingIntervalInMs = 10 * 1000;
|
|
9340
|
+
this.unhealthyTimeoutInMs = this.pingIntervalInMs + 5 * 1000;
|
|
9341
|
+
this.close = (code, reason) => {
|
|
9342
|
+
this.logger('debug', `Closing SFU WS connection: ${code} - ${reason}`);
|
|
9343
|
+
if (this.signalWs.readyState !== this.signalWs.CLOSED) {
|
|
9745
9344
|
this.signalWs.close(code, `js-client: ${reason}`);
|
|
9746
|
-
this.cleanUpWebSocket();
|
|
9747
9345
|
}
|
|
9748
|
-
this.dispose();
|
|
9749
|
-
};
|
|
9750
|
-
this.dispose = () => {
|
|
9751
|
-
this.logger('debug', 'Disposing SFU client');
|
|
9752
9346
|
this.unsubscribeIceTrickle();
|
|
9753
9347
|
clearInterval(this.keepAliveInterval);
|
|
9754
9348
|
clearTimeout(this.connectionCheckTimeout);
|
|
9755
|
-
clearTimeout(this.migrateAwayTimeout);
|
|
9756
|
-
this.abortController.abort();
|
|
9757
|
-
this.migrationTask?.resolve();
|
|
9758
|
-
};
|
|
9759
|
-
this.leaveAndClose = async (reason) => {
|
|
9760
|
-
await this.joinResponseTask.promise;
|
|
9761
|
-
try {
|
|
9762
|
-
this.isLeaving = true;
|
|
9763
|
-
await this.notifyLeave(reason);
|
|
9764
|
-
}
|
|
9765
|
-
catch (err) {
|
|
9766
|
-
this.logger('debug', 'Error notifying SFU about leaving call', err);
|
|
9767
|
-
}
|
|
9768
|
-
this.close(StreamSfuClient.NORMAL_CLOSURE, reason.substring(0, 115));
|
|
9769
9349
|
};
|
|
9770
|
-
this.updateSubscriptions = async (
|
|
9771
|
-
|
|
9772
|
-
|
|
9350
|
+
this.updateSubscriptions = async (subscriptions) => {
|
|
9351
|
+
return retryable(() => this.rpc.updateSubscriptions({
|
|
9352
|
+
sessionId: this.sessionId,
|
|
9353
|
+
tracks: subscriptions,
|
|
9354
|
+
}), this.logger, 'debug');
|
|
9773
9355
|
};
|
|
9774
9356
|
this.setPublisher = async (data) => {
|
|
9775
|
-
|
|
9776
|
-
|
|
9357
|
+
return retryable(() => this.rpc.setPublisher({
|
|
9358
|
+
...data,
|
|
9359
|
+
sessionId: this.sessionId,
|
|
9360
|
+
}), this.logger);
|
|
9777
9361
|
};
|
|
9778
9362
|
this.sendAnswer = async (data) => {
|
|
9779
|
-
|
|
9780
|
-
|
|
9363
|
+
return retryable(() => this.rpc.sendAnswer({
|
|
9364
|
+
...data,
|
|
9365
|
+
sessionId: this.sessionId,
|
|
9366
|
+
}), this.logger);
|
|
9781
9367
|
};
|
|
9782
9368
|
this.iceTrickle = async (data) => {
|
|
9783
|
-
|
|
9784
|
-
|
|
9369
|
+
return retryable(() => this.rpc.iceTrickle({
|
|
9370
|
+
...data,
|
|
9371
|
+
sessionId: this.sessionId,
|
|
9372
|
+
}), this.logger);
|
|
9785
9373
|
};
|
|
9786
9374
|
this.iceRestart = async (data) => {
|
|
9787
|
-
|
|
9788
|
-
|
|
9375
|
+
return retryable(() => this.rpc.iceRestart({
|
|
9376
|
+
...data,
|
|
9377
|
+
sessionId: this.sessionId,
|
|
9378
|
+
}), this.logger);
|
|
9789
9379
|
};
|
|
9790
9380
|
this.updateMuteState = async (trackType, muted) => {
|
|
9791
|
-
|
|
9792
|
-
|
|
9381
|
+
return this.updateMuteStates({
|
|
9382
|
+
muteStates: [
|
|
9383
|
+
{
|
|
9384
|
+
trackType,
|
|
9385
|
+
muted,
|
|
9386
|
+
},
|
|
9387
|
+
],
|
|
9388
|
+
});
|
|
9793
9389
|
};
|
|
9794
9390
|
this.updateMuteStates = async (data) => {
|
|
9795
|
-
|
|
9796
|
-
|
|
9391
|
+
return retryable(() => this.rpc.updateMuteStates({
|
|
9392
|
+
...data,
|
|
9393
|
+
sessionId: this.sessionId,
|
|
9394
|
+
}), this.logger);
|
|
9797
9395
|
};
|
|
9798
9396
|
this.sendStats = async (stats) => {
|
|
9799
|
-
|
|
9800
|
-
|
|
9397
|
+
return retryable(() => this.rpc.sendStats({
|
|
9398
|
+
...stats,
|
|
9399
|
+
sessionId: this.sessionId,
|
|
9400
|
+
}), this.logger, 'debug');
|
|
9801
9401
|
};
|
|
9802
9402
|
this.startNoiseCancellation = async () => {
|
|
9803
|
-
|
|
9804
|
-
|
|
9403
|
+
return retryable(() => this.rpc.startNoiseCancellation({
|
|
9404
|
+
sessionId: this.sessionId,
|
|
9405
|
+
}), this.logger);
|
|
9805
9406
|
};
|
|
9806
9407
|
this.stopNoiseCancellation = async () => {
|
|
9807
|
-
|
|
9808
|
-
|
|
9809
|
-
|
|
9810
|
-
this.enterMigration = async (opts = {}) => {
|
|
9811
|
-
this.isLeaving = true;
|
|
9812
|
-
const { timeout = 10000 } = opts;
|
|
9813
|
-
this.migrationTask?.reject(new Error('Cancelled previous migration'));
|
|
9814
|
-
this.migrationTask = promiseWithResolvers();
|
|
9815
|
-
const task = this.migrationTask;
|
|
9816
|
-
const unsubscribe = this.dispatcher.on('participantMigrationComplete', () => {
|
|
9817
|
-
unsubscribe();
|
|
9818
|
-
clearTimeout(this.migrateAwayTimeout);
|
|
9819
|
-
task.resolve();
|
|
9820
|
-
});
|
|
9821
|
-
this.migrateAwayTimeout = setTimeout(() => {
|
|
9822
|
-
unsubscribe();
|
|
9823
|
-
// task.reject(new Error('Migration timeout'));
|
|
9824
|
-
// FIXME OL: temporary, switch to `task.reject()` once the SFU starts sending
|
|
9825
|
-
// the participantMigrationComplete event.
|
|
9826
|
-
task.resolve();
|
|
9827
|
-
}, timeout);
|
|
9828
|
-
return this.migrationTask.promise;
|
|
9408
|
+
return retryable(() => this.rpc.stopNoiseCancellation({
|
|
9409
|
+
sessionId: this.sessionId,
|
|
9410
|
+
}), this.logger);
|
|
9829
9411
|
};
|
|
9830
9412
|
this.join = async (data) => {
|
|
9831
|
-
|
|
9832
|
-
|
|
9833
|
-
|
|
9834
|
-
|
|
9835
|
-
// that's why we have this primitive lock mechanism.
|
|
9836
|
-
// the client starts with already initialized joinResponseTask,
|
|
9837
|
-
// and this code creates a new one for the next join request.
|
|
9838
|
-
this.joinResponseTask = promiseWithResolvers();
|
|
9839
|
-
}
|
|
9840
|
-
// capture a reference to the current joinResponseTask as it might
|
|
9841
|
-
// be replaced with a new one in case a second join request is made
|
|
9842
|
-
const current = this.joinResponseTask;
|
|
9843
|
-
let timeoutId;
|
|
9844
|
-
const unsubscribe = this.dispatcher.on('joinResponse', (joinResponse) => {
|
|
9845
|
-
this.logger('debug', 'Received joinResponse', joinResponse);
|
|
9846
|
-
clearTimeout(timeoutId);
|
|
9847
|
-
unsubscribe();
|
|
9848
|
-
this.keepAlive();
|
|
9849
|
-
current.resolve(joinResponse);
|
|
9413
|
+
const joinRequest = JoinRequest.create({
|
|
9414
|
+
...data,
|
|
9415
|
+
sessionId: this.sessionId,
|
|
9416
|
+
token: this.token,
|
|
9850
9417
|
});
|
|
9851
|
-
timeoutId = setTimeout(() => {
|
|
9852
|
-
unsubscribe();
|
|
9853
|
-
current.reject(new Error('Waiting for "joinResponse" has timed out'));
|
|
9854
|
-
}, this.joinResponseTimeout);
|
|
9855
|
-
await this.send(SfuRequest.create({
|
|
9856
|
-
requestPayload: {
|
|
9857
|
-
oneofKind: 'joinRequest',
|
|
9858
|
-
joinRequest: JoinRequest.create({
|
|
9859
|
-
...data,
|
|
9860
|
-
sessionId: this.sessionId,
|
|
9861
|
-
token: this.credentials.token,
|
|
9862
|
-
}),
|
|
9863
|
-
},
|
|
9864
|
-
}));
|
|
9865
|
-
return current.promise;
|
|
9866
|
-
};
|
|
9867
|
-
this.ping = async () => {
|
|
9868
|
-
return this.send(SfuRequest.create({
|
|
9869
|
-
requestPayload: {
|
|
9870
|
-
oneofKind: 'healthCheckRequest',
|
|
9871
|
-
healthCheckRequest: {},
|
|
9872
|
-
},
|
|
9873
|
-
}));
|
|
9874
|
-
};
|
|
9875
|
-
this.notifyLeave = async (reason) => {
|
|
9876
9418
|
return this.send(SfuRequest.create({
|
|
9877
9419
|
requestPayload: {
|
|
9878
|
-
oneofKind: '
|
|
9879
|
-
|
|
9880
|
-
sessionId: this.sessionId,
|
|
9881
|
-
reason,
|
|
9882
|
-
},
|
|
9420
|
+
oneofKind: 'joinRequest',
|
|
9421
|
+
joinRequest,
|
|
9883
9422
|
},
|
|
9884
9423
|
}));
|
|
9885
9424
|
};
|
|
9886
9425
|
this.send = async (message) => {
|
|
9887
|
-
|
|
9888
|
-
|
|
9889
|
-
|
|
9890
|
-
this.logger('debug',
|
|
9891
|
-
|
|
9892
|
-
}
|
|
9893
|
-
this.logger('debug', `Sending message to: ${this.edgeName}`, msgJson);
|
|
9894
|
-
this.signalWs.send(SfuRequest.toBinary(message));
|
|
9426
|
+
return this.signalReady.then((signal) => {
|
|
9427
|
+
if (signal.readyState !== signal.OPEN)
|
|
9428
|
+
return;
|
|
9429
|
+
this.logger('debug', `Sending message to: ${this.edgeName}`, SfuRequest.toJson(message));
|
|
9430
|
+
signal.send(SfuRequest.toBinary(message));
|
|
9431
|
+
});
|
|
9895
9432
|
};
|
|
9896
9433
|
this.keepAlive = () => {
|
|
9897
9434
|
clearInterval(this.keepAliveInterval);
|
|
9898
9435
|
this.keepAliveInterval = setInterval(() => {
|
|
9899
|
-
this.
|
|
9436
|
+
this.logger('trace', 'Sending healthCheckRequest to SFU');
|
|
9437
|
+
const message = SfuRequest.create({
|
|
9438
|
+
requestPayload: {
|
|
9439
|
+
oneofKind: 'healthCheckRequest',
|
|
9440
|
+
healthCheckRequest: {},
|
|
9441
|
+
},
|
|
9442
|
+
});
|
|
9443
|
+
this.send(message).catch((e) => {
|
|
9900
9444
|
this.logger('error', 'Error sending healthCheckRequest to SFU', e);
|
|
9901
9445
|
});
|
|
9902
9446
|
}, this.pingIntervalInMs);
|
|
@@ -9912,37 +9456,53 @@ class StreamSfuClient {
|
|
|
9912
9456
|
}
|
|
9913
9457
|
}, this.unhealthyTimeoutInMs);
|
|
9914
9458
|
};
|
|
9915
|
-
this.dispatcher = dispatcher;
|
|
9916
9459
|
this.sessionId = sessionId || generateUUIDv4();
|
|
9917
|
-
this.
|
|
9918
|
-
this.
|
|
9919
|
-
|
|
9920
|
-
this.
|
|
9921
|
-
|
|
9922
|
-
|
|
9923
|
-
|
|
9460
|
+
this.sfuServer = sfuServer;
|
|
9461
|
+
this.edgeName = sfuServer.edge_name;
|
|
9462
|
+
this.token = token;
|
|
9463
|
+
this.logger = getLogger(['sfu-client']);
|
|
9464
|
+
const logInterceptor = {
|
|
9465
|
+
interceptUnary: (next, method, input, options) => {
|
|
9466
|
+
this.logger('trace', `Calling SFU RPC method ${method.name}`, {
|
|
9467
|
+
input,
|
|
9468
|
+
options,
|
|
9469
|
+
});
|
|
9470
|
+
return next(method, input, options);
|
|
9471
|
+
},
|
|
9472
|
+
};
|
|
9924
9473
|
this.rpc = createSignalClient({
|
|
9925
|
-
baseUrl:
|
|
9474
|
+
baseUrl: sfuServer.url,
|
|
9926
9475
|
interceptors: [
|
|
9927
9476
|
withHeaders({
|
|
9928
9477
|
Authorization: `Bearer ${token}`,
|
|
9929
9478
|
}),
|
|
9930
|
-
|
|
9931
|
-
]
|
|
9479
|
+
logInterceptor,
|
|
9480
|
+
],
|
|
9932
9481
|
});
|
|
9933
9482
|
// Special handling for the ICETrickle kind of events.
|
|
9934
|
-
//
|
|
9935
|
-
// connection is established
|
|
9936
|
-
//
|
|
9937
|
-
// and later added to the appropriate PeerConnection
|
|
9483
|
+
// These events might be triggered by the SFU before the initial RTC
|
|
9484
|
+
// connection is established. In that case, those events (ICE candidates)
|
|
9485
|
+
// need to be buffered and later added to the appropriate PeerConnection
|
|
9938
9486
|
// once the remoteDescription is known and set.
|
|
9939
9487
|
this.unsubscribeIceTrickle = dispatcher.on('iceTrickle', (iceTrickle) => {
|
|
9940
9488
|
this.iceTrickleBuffer.push(iceTrickle);
|
|
9941
9489
|
});
|
|
9942
|
-
this.
|
|
9943
|
-
|
|
9944
|
-
|
|
9945
|
-
|
|
9490
|
+
this.signalWs = createWebSocketSignalChannel({
|
|
9491
|
+
endpoint: sfuServer.ws_endpoint,
|
|
9492
|
+
onMessage: (message) => {
|
|
9493
|
+
this.lastMessageTimestamp = new Date();
|
|
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
|
+
});
|
|
9946
9506
|
}
|
|
9947
9507
|
}
|
|
9948
9508
|
/**
|
|
@@ -9955,15 +9515,45 @@ StreamSfuClient.NORMAL_CLOSURE = 1000;
|
|
|
9955
9515
|
* a certain amount of time (`connectionCheckTimeout`).
|
|
9956
9516
|
*/
|
|
9957
9517
|
StreamSfuClient.ERROR_CONNECTION_UNHEALTHY = 4001;
|
|
9958
|
-
|
|
9959
|
-
|
|
9960
|
-
|
|
9961
|
-
|
|
9962
|
-
|
|
9963
|
-
|
|
9964
|
-
|
|
9965
|
-
|
|
9966
|
-
|
|
9518
|
+
/**
|
|
9519
|
+
* The error code used when the SFU connection is broken.
|
|
9520
|
+
* Usually, this means that the WS connection has been closed unexpectedly.
|
|
9521
|
+
* This error code is used to announce a fast-reconnect.
|
|
9522
|
+
*/
|
|
9523
|
+
StreamSfuClient.ERROR_CONNECTION_BROKEN = 4002; // used in fast-reconnects
|
|
9524
|
+
const MAX_RETRIES = 5;
|
|
9525
|
+
/**
|
|
9526
|
+
* Creates a closure which wraps the given RPC call and retries invoking
|
|
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;
|
|
9967
9557
|
};
|
|
9968
9558
|
|
|
9969
9559
|
/**
|
|
@@ -10118,10 +9708,9 @@ const watchSfuErrorReports = (dispatcher) => {
|
|
|
10118
9708
|
return dispatcher.on('error', (e) => {
|
|
10119
9709
|
if (!e.error)
|
|
10120
9710
|
return;
|
|
10121
|
-
const { error
|
|
9711
|
+
const { error } = e;
|
|
10122
9712
|
logger$1('error', 'SFU reported error', {
|
|
10123
9713
|
code: ErrorCode[error.code],
|
|
10124
|
-
reconnectStrategy: WebsocketReconnectStrategy[reconnectStrategy],
|
|
10125
9714
|
message: error.message,
|
|
10126
9715
|
shouldRetry: error.shouldRetry,
|
|
10127
9716
|
});
|
|
@@ -10137,17 +9726,6 @@ const watchPinsUpdated = (state) => {
|
|
|
10137
9726
|
state.setServerSidePins(pins);
|
|
10138
9727
|
};
|
|
10139
9728
|
};
|
|
10140
|
-
/**
|
|
10141
|
-
* Watches for `callEnded` events.
|
|
10142
|
-
*/
|
|
10143
|
-
const watchSfuCallEnded = (call) => {
|
|
10144
|
-
return call.on('callEnded', (e) => {
|
|
10145
|
-
const reason = CallEndedReason[e.reason];
|
|
10146
|
-
call.leave({ reason }).catch((err) => {
|
|
10147
|
-
logger$1('error', 'Failed to leave call after call ended by the SFU', err);
|
|
10148
|
-
});
|
|
10149
|
-
});
|
|
10150
|
-
};
|
|
10151
9729
|
|
|
10152
9730
|
/**
|
|
10153
9731
|
* An event handler that handles soft mutes.
|
|
@@ -10169,13 +9747,12 @@ const handleRemoteSoftMute = (call) => {
|
|
|
10169
9747
|
else if (type === TrackType.AUDIO) {
|
|
10170
9748
|
await call.microphone.disable();
|
|
10171
9749
|
}
|
|
10172
|
-
else if (type === TrackType.SCREEN_SHARE ||
|
|
10173
|
-
type === TrackType.SCREEN_SHARE_AUDIO) {
|
|
10174
|
-
await call.screenShare.disable();
|
|
10175
|
-
}
|
|
10176
9750
|
else {
|
|
10177
9751
|
logger('warn', 'Unsupported track type to soft mute', TrackType[type]);
|
|
10178
9752
|
}
|
|
9753
|
+
if (call.publisher?.isPublishing(type)) {
|
|
9754
|
+
await call.stopPublish(type);
|
|
9755
|
+
}
|
|
10179
9756
|
}
|
|
10180
9757
|
catch (error) {
|
|
10181
9758
|
logger('error', 'Failed to stop publishing', error);
|
|
@@ -10196,12 +9773,11 @@ const watchParticipantJoined = (state) => {
|
|
|
10196
9773
|
// potential duplicate events from the SFU.
|
|
10197
9774
|
//
|
|
10198
9775
|
// Although the SFU should not send duplicate events, we have seen
|
|
10199
|
-
// some race conditions in the past during the `join-flow
|
|
10200
|
-
//
|
|
9776
|
+
// some race conditions in the past during the `join-flow` where
|
|
9777
|
+
// the SFU would send participant info as part of the `join`
|
|
10201
9778
|
// response and then follow up with a `participantJoined` event for
|
|
10202
9779
|
// already announced participants.
|
|
10203
|
-
|
|
10204
|
-
state.updateOrAddParticipant(participant.sessionId, Object.assign(participant, orphanedTracks, {
|
|
9780
|
+
state.updateOrAddParticipant(participant.sessionId, Object.assign(participant, {
|
|
10205
9781
|
viewportVisibilityState: {
|
|
10206
9782
|
videoTrack: VisibilityState.UNKNOWN,
|
|
10207
9783
|
screenShareTrack: VisibilityState.UNKNOWN,
|
|
@@ -10237,14 +9813,12 @@ const watchParticipantUpdated = (state) => {
|
|
|
10237
9813
|
*/
|
|
10238
9814
|
const watchTrackPublished = (state) => {
|
|
10239
9815
|
return function onTrackPublished(e) {
|
|
10240
|
-
const { type, sessionId } = e;
|
|
9816
|
+
const { type, sessionId, participant } = e;
|
|
10241
9817
|
// An optimization for large calls.
|
|
10242
9818
|
// After a certain threshold, the SFU would stop emitting `participantJoined`
|
|
10243
9819
|
// events, and instead, it would only provide the participant's information
|
|
10244
9820
|
// once they start publishing a track.
|
|
10245
|
-
if (
|
|
10246
|
-
const orphanedTracks = reconcileOrphanedTracks(state, e.participant);
|
|
10247
|
-
const participant = Object.assign(e.participant, orphanedTracks);
|
|
9821
|
+
if (participant) {
|
|
10248
9822
|
state.updateOrAddParticipant(sessionId, participant);
|
|
10249
9823
|
}
|
|
10250
9824
|
else {
|
|
@@ -10260,11 +9834,9 @@ const watchTrackPublished = (state) => {
|
|
|
10260
9834
|
*/
|
|
10261
9835
|
const watchTrackUnpublished = (state) => {
|
|
10262
9836
|
return function onTrackUnpublished(e) {
|
|
10263
|
-
const { type, sessionId } = e;
|
|
9837
|
+
const { type, sessionId, participant } = e;
|
|
10264
9838
|
// An optimization for large calls. See `watchTrackPublished`.
|
|
10265
|
-
if (
|
|
10266
|
-
const orphanedTracks = reconcileOrphanedTracks(state, e.participant);
|
|
10267
|
-
const participant = Object.assign(e.participant, orphanedTracks);
|
|
9839
|
+
if (participant) {
|
|
10268
9840
|
state.updateOrAddParticipant(sessionId, participant);
|
|
10269
9841
|
}
|
|
10270
9842
|
else {
|
|
@@ -10275,25 +9847,6 @@ const watchTrackUnpublished = (state) => {
|
|
|
10275
9847
|
};
|
|
10276
9848
|
};
|
|
10277
9849
|
const unique = (v, i, arr) => arr.indexOf(v) === i;
|
|
10278
|
-
/**
|
|
10279
|
-
* Reconciles orphaned tracks (if any) for the given participant.
|
|
10280
|
-
*
|
|
10281
|
-
* @param state the call state.
|
|
10282
|
-
* @param participant the participant.
|
|
10283
|
-
*/
|
|
10284
|
-
const reconcileOrphanedTracks = (state, participant) => {
|
|
10285
|
-
const orphanTracks = state.takeOrphanedTracks(participant.trackLookupPrefix);
|
|
10286
|
-
if (!orphanTracks.length)
|
|
10287
|
-
return;
|
|
10288
|
-
const reconciledTracks = {};
|
|
10289
|
-
for (const orphan of orphanTracks) {
|
|
10290
|
-
const key = trackTypeToParticipantStreamKey(orphan.trackType);
|
|
10291
|
-
if (!key)
|
|
10292
|
-
continue;
|
|
10293
|
-
reconciledTracks[key] = orphan.track;
|
|
10294
|
-
}
|
|
10295
|
-
return reconciledTracks;
|
|
10296
|
-
};
|
|
10297
9850
|
|
|
10298
9851
|
/**
|
|
10299
9852
|
* Watches for `dominantSpeakerChanged` events.
|
|
@@ -10342,13 +9895,12 @@ const watchAudioLevelChanged = (dispatcher, state) => {
|
|
|
10342
9895
|
* Registers the default event handlers for a call during its lifecycle.
|
|
10343
9896
|
*
|
|
10344
9897
|
* @param call the call to register event handlers for.
|
|
9898
|
+
* @param state the call state.
|
|
10345
9899
|
* @param dispatcher the dispatcher.
|
|
10346
9900
|
*/
|
|
10347
|
-
const registerEventHandlers = (call, dispatcher) => {
|
|
10348
|
-
const state = call.state;
|
|
9901
|
+
const registerEventHandlers = (call, state, dispatcher) => {
|
|
10349
9902
|
const eventHandlers = [
|
|
10350
9903
|
call.on('call.ended', watchCallEnded(call)),
|
|
10351
|
-
watchSfuCallEnded(call),
|
|
10352
9904
|
watchLiveEnded(dispatcher, call),
|
|
10353
9905
|
watchSfuErrorReports(dispatcher),
|
|
10354
9906
|
watchChangePublishQuality(dispatcher, call),
|
|
@@ -10392,6 +9944,48 @@ const registerRingingCallEventHandlers = (call) => {
|
|
|
10392
9944
|
};
|
|
10393
9945
|
};
|
|
10394
9946
|
|
|
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
|
+
|
|
10395
9989
|
/**
|
|
10396
9990
|
* Flatten the stats report into an array of stats objects.
|
|
10397
9991
|
*
|
|
@@ -10671,7 +10265,6 @@ class SfuStatsReporter {
|
|
|
10671
10265
|
this.start = () => {
|
|
10672
10266
|
if (this.options.reporting_interval_ms <= 0)
|
|
10673
10267
|
return;
|
|
10674
|
-
clearInterval(this.intervalId);
|
|
10675
10268
|
this.intervalId = setInterval(() => {
|
|
10676
10269
|
this.run().catch((err) => {
|
|
10677
10270
|
this.logger('warn', 'Failed to report stats', err);
|
|
@@ -11244,13 +10837,9 @@ class BrowserPermission {
|
|
|
11244
10837
|
const signal = this.disposeController.signal;
|
|
11245
10838
|
this.ready = (async () => {
|
|
11246
10839
|
const assumeGranted = (error) => {
|
|
11247
|
-
this.logger('warn', "Can't query permissions, assuming granted", {
|
|
11248
|
-
permission,
|
|
11249
|
-
error,
|
|
11250
|
-
});
|
|
11251
10840
|
this.setState('granted');
|
|
11252
10841
|
};
|
|
11253
|
-
if (!canQueryPermissions()
|
|
10842
|
+
if (!canQueryPermissions()) {
|
|
11254
10843
|
return assumeGranted();
|
|
11255
10844
|
}
|
|
11256
10845
|
try {
|
|
@@ -11265,7 +10854,7 @@ class BrowserPermission {
|
|
|
11265
10854
|
}
|
|
11266
10855
|
}
|
|
11267
10856
|
catch (err) {
|
|
11268
|
-
assumeGranted(
|
|
10857
|
+
assumeGranted();
|
|
11269
10858
|
}
|
|
11270
10859
|
})();
|
|
11271
10860
|
}
|
|
@@ -11359,7 +10948,7 @@ function lazy(factory) {
|
|
|
11359
10948
|
* Returns an Observable that emits the list of available devices
|
|
11360
10949
|
* that meet the given constraints.
|
|
11361
10950
|
*
|
|
11362
|
-
* @param
|
|
10951
|
+
* @param constraints the constraints to use when requesting the devices.
|
|
11363
10952
|
* @param kind the kind of devices to enumerate.
|
|
11364
10953
|
*/
|
|
11365
10954
|
const getDevices = (permission, kind) => {
|
|
@@ -11612,12 +11201,6 @@ class InputMediaDeviceManager {
|
|
|
11612
11201
|
listDevices() {
|
|
11613
11202
|
return this.getDevices();
|
|
11614
11203
|
}
|
|
11615
|
-
/**
|
|
11616
|
-
* Returns `true` when this device is in enabled state.
|
|
11617
|
-
*/
|
|
11618
|
-
get enabled() {
|
|
11619
|
-
return this.state.status === 'enabled';
|
|
11620
|
-
}
|
|
11621
11204
|
/**
|
|
11622
11205
|
* Starts stream.
|
|
11623
11206
|
*/
|
|
@@ -11740,7 +11323,7 @@ class InputMediaDeviceManager {
|
|
|
11740
11323
|
await this.applySettingsToStream();
|
|
11741
11324
|
}
|
|
11742
11325
|
async applySettingsToStream() {
|
|
11743
|
-
if (this.enabled) {
|
|
11326
|
+
if (this.state.status === 'enabled') {
|
|
11744
11327
|
await this.muteStream();
|
|
11745
11328
|
await this.unmuteStream();
|
|
11746
11329
|
}
|
|
@@ -11877,22 +11460,19 @@ class InputMediaDeviceManager {
|
|
|
11877
11460
|
return output;
|
|
11878
11461
|
})
|
|
11879
11462
|
.then(chainWith(parent), (error) => {
|
|
11880
|
-
this.logger('warn', '
|
|
11463
|
+
this.logger('warn', 'Fitler failed to start and will be ignored', error);
|
|
11881
11464
|
return parent;
|
|
11882
11465
|
}), rootStream);
|
|
11883
11466
|
}
|
|
11884
11467
|
if (this.call.state.callingState === CallingState.JOINED) {
|
|
11885
11468
|
await this.publishStream(stream);
|
|
11886
11469
|
}
|
|
11887
|
-
else {
|
|
11888
|
-
this.logger('debug', 'Stream is not published as the call is not joined');
|
|
11889
|
-
}
|
|
11890
11470
|
if (this.state.mediaStream !== stream) {
|
|
11891
11471
|
this.state.setMediaStream(stream, await rootStream);
|
|
11892
11472
|
this.getTracks().forEach((track) => {
|
|
11893
11473
|
track.addEventListener('ended', async () => {
|
|
11894
11474
|
await this.statusChangeSettled();
|
|
11895
|
-
if (this.enabled) {
|
|
11475
|
+
if (this.state.status === 'enabled') {
|
|
11896
11476
|
this.isTrackStoppedDueToTrackEnd = true;
|
|
11897
11477
|
setTimeout(() => {
|
|
11898
11478
|
this.isTrackStoppedDueToTrackEnd = false;
|
|
@@ -12185,7 +11765,7 @@ class CameraManager extends InputMediaDeviceManager {
|
|
|
12185
11765
|
this.logger('warn', 'could not apply target resolution', error);
|
|
12186
11766
|
}
|
|
12187
11767
|
}
|
|
12188
|
-
if (this.enabled) {
|
|
11768
|
+
if (this.state.status === 'enabled') {
|
|
12189
11769
|
const { width, height } = this.state
|
|
12190
11770
|
.mediaStream.getVideoTracks()[0]
|
|
12191
11771
|
?.getSettings();
|
|
@@ -12489,7 +12069,7 @@ class MicrophoneManager extends InputMediaDeviceManager {
|
|
|
12489
12069
|
});
|
|
12490
12070
|
const registrationResult = this.registerFilter(noiseCancellation.toFilter());
|
|
12491
12071
|
this.noiseCancellationRegistration = registrationResult.registered;
|
|
12492
|
-
this.
|
|
12072
|
+
this.uregisterNoiseCancellation = registrationResult.unregister;
|
|
12493
12073
|
await this.noiseCancellationRegistration;
|
|
12494
12074
|
// handles an edge case where a noise cancellation is enabled after
|
|
12495
12075
|
// the participant as joined the call -> we immediately enable NC
|
|
@@ -12515,7 +12095,7 @@ class MicrophoneManager extends InputMediaDeviceManager {
|
|
|
12515
12095
|
if (isReactNative()) {
|
|
12516
12096
|
throw new Error('Noise cancellation is not supported in React Native');
|
|
12517
12097
|
}
|
|
12518
|
-
await (this.
|
|
12098
|
+
await (this.uregisterNoiseCancellation?.() ?? Promise.resolve())
|
|
12519
12099
|
.then(() => this.noiseCancellation?.disable())
|
|
12520
12100
|
.then(() => this.noiseCancellationChangeUnsubscribe?.())
|
|
12521
12101
|
.catch((err) => {
|
|
@@ -12897,17 +12477,9 @@ class Call {
|
|
|
12897
12477
|
*/
|
|
12898
12478
|
this.dispatcher = new Dispatcher();
|
|
12899
12479
|
this.trackSubscriptionsSubject = new BehaviorSubject({ type: DebounceType.MEDIUM, data: [] });
|
|
12900
|
-
this.sfuClientTag = 0;
|
|
12901
|
-
this.reconnectConcurrencyTag = Symbol('reconnectConcurrencyTag');
|
|
12902
12480
|
this.reconnectAttempts = 0;
|
|
12903
|
-
this.
|
|
12904
|
-
this.
|
|
12905
|
-
this.lastOfflineTimestamp = 0;
|
|
12906
|
-
// maintain the order of publishing tracks to restore them after a reconnection
|
|
12907
|
-
// it shouldn't contain duplicates
|
|
12908
|
-
this.trackPublishOrder = [];
|
|
12909
|
-
this.hasJoinedOnce = false;
|
|
12910
|
-
this.deviceSettingsAppliedOnce = false;
|
|
12481
|
+
this.maxReconnectAttempts = 10;
|
|
12482
|
+
this.isLeaving = false;
|
|
12911
12483
|
this.initialized = false;
|
|
12912
12484
|
this.joinLeaveConcurrencyTag = Symbol('joinLeaveConcurrencyTag');
|
|
12913
12485
|
/**
|
|
@@ -12917,42 +12489,6 @@ class Call {
|
|
|
12917
12489
|
*/
|
|
12918
12490
|
this.leaveCallHooks = new Set();
|
|
12919
12491
|
this.streamClientEventHandlers = new Map();
|
|
12920
|
-
this.handleOwnCapabilitiesUpdated = async (ownCapabilities) => {
|
|
12921
|
-
// update the permission context.
|
|
12922
|
-
this.permissionsContext.setPermissions(ownCapabilities);
|
|
12923
|
-
if (!this.publisher)
|
|
12924
|
-
return;
|
|
12925
|
-
// check if the user still has publishing permissions and stop publishing if not.
|
|
12926
|
-
const permissionToTrackType = {
|
|
12927
|
-
[OwnCapability.SEND_AUDIO]: TrackType.AUDIO,
|
|
12928
|
-
[OwnCapability.SEND_VIDEO]: TrackType.VIDEO,
|
|
12929
|
-
[OwnCapability.SCREENSHARE]: TrackType.SCREEN_SHARE,
|
|
12930
|
-
};
|
|
12931
|
-
for (const [permission, trackType] of Object.entries(permissionToTrackType)) {
|
|
12932
|
-
const hasPermission = this.permissionsContext.hasPermission(permission);
|
|
12933
|
-
if (hasPermission)
|
|
12934
|
-
continue;
|
|
12935
|
-
try {
|
|
12936
|
-
switch (trackType) {
|
|
12937
|
-
case TrackType.AUDIO:
|
|
12938
|
-
if (this.microphone.enabled)
|
|
12939
|
-
await this.microphone.disable();
|
|
12940
|
-
break;
|
|
12941
|
-
case TrackType.VIDEO:
|
|
12942
|
-
if (this.camera.enabled)
|
|
12943
|
-
await this.camera.disable();
|
|
12944
|
-
break;
|
|
12945
|
-
case TrackType.SCREEN_SHARE:
|
|
12946
|
-
if (this.screenShare.enabled)
|
|
12947
|
-
await this.screenShare.disable();
|
|
12948
|
-
break;
|
|
12949
|
-
}
|
|
12950
|
-
}
|
|
12951
|
-
catch (err) {
|
|
12952
|
-
this.logger('error', `Can't disable mic/camera/screenshare after revoked permissions`, err);
|
|
12953
|
-
}
|
|
12954
|
-
}
|
|
12955
|
-
};
|
|
12956
12492
|
/**
|
|
12957
12493
|
* You can subscribe to WebSocket events provided by the API. To remove a subscription, call the `off` method.
|
|
12958
12494
|
* Please note that subscribing to WebSocket events is an advanced use-case.
|
|
@@ -13003,8 +12539,9 @@ class Call {
|
|
|
13003
12539
|
throw new Error('Cannot leave call that has already been left.');
|
|
13004
12540
|
}
|
|
13005
12541
|
if (callingState === CallingState.JOINING) {
|
|
13006
|
-
await this.
|
|
12542
|
+
await this.assertCallJoined();
|
|
13007
12543
|
}
|
|
12544
|
+
this.isLeaving = true;
|
|
13008
12545
|
if (this.ringing) {
|
|
13009
12546
|
// I'm the one who started the call, so I should cancel it.
|
|
13010
12547
|
const hasOtherParticipants = this.state.remoteParticipants.length > 0;
|
|
@@ -13026,15 +12563,14 @@ class Call {
|
|
|
13026
12563
|
this.sfuStatsReporter = undefined;
|
|
13027
12564
|
this.subscriber?.close();
|
|
13028
12565
|
this.subscriber = undefined;
|
|
13029
|
-
this.publisher?.close(
|
|
12566
|
+
this.publisher?.close();
|
|
13030
12567
|
this.publisher = undefined;
|
|
13031
|
-
|
|
12568
|
+
this.sfuClient?.close(StreamSfuClient.NORMAL_CLOSURE, reason);
|
|
13032
12569
|
this.sfuClient = undefined;
|
|
13033
12570
|
this.state.setCallingState(CallingState.LEFT);
|
|
13034
12571
|
// Call all leave call hooks, e.g. to clean up global event handlers
|
|
13035
12572
|
this.leaveCallHooks.forEach((hook) => hook());
|
|
13036
12573
|
this.initialized = false;
|
|
13037
|
-
this.hasJoinedOnce = false;
|
|
13038
12574
|
this.clientStore.unregisterCall(this);
|
|
13039
12575
|
this.camera.dispose();
|
|
13040
12576
|
this.microphone.dispose();
|
|
@@ -13074,7 +12610,7 @@ class Call {
|
|
|
13074
12610
|
this.watching = true;
|
|
13075
12611
|
this.clientStore.registerCall(this);
|
|
13076
12612
|
}
|
|
13077
|
-
await this.applyDeviceConfig(
|
|
12613
|
+
await this.applyDeviceConfig();
|
|
13078
12614
|
return response;
|
|
13079
12615
|
};
|
|
13080
12616
|
/**
|
|
@@ -13096,7 +12632,7 @@ class Call {
|
|
|
13096
12632
|
this.watching = true;
|
|
13097
12633
|
this.clientStore.registerCall(this);
|
|
13098
12634
|
}
|
|
13099
|
-
await this.applyDeviceConfig(
|
|
12635
|
+
await this.applyDeviceConfig();
|
|
13100
12636
|
return response;
|
|
13101
12637
|
};
|
|
13102
12638
|
/**
|
|
@@ -13152,171 +12688,253 @@ class Call {
|
|
|
13152
12688
|
await this.setup();
|
|
13153
12689
|
const callingState = this.state.callingState;
|
|
13154
12690
|
if ([CallingState.JOINED, CallingState.JOINING].includes(callingState)) {
|
|
13155
|
-
|
|
12691
|
+
this.logger('warn', 'Join method called twice, you should only call this once');
|
|
12692
|
+
throw new Error(`Illegal State: Already joined.`);
|
|
13156
12693
|
}
|
|
13157
|
-
|
|
13158
|
-
|
|
12694
|
+
const isMigrating = callingState === CallingState.MIGRATING;
|
|
12695
|
+
const isReconnecting = callingState === CallingState.RECONNECTING;
|
|
13159
12696
|
this.state.setCallingState(CallingState.JOINING);
|
|
13160
|
-
|
|
13161
|
-
|
|
13162
|
-
|
|
13163
|
-
|
|
13164
|
-
if (!this.
|
|
13165
|
-
|
|
13166
|
-
|
|
13167
|
-
|
|
13168
|
-
|
|
13169
|
-
|
|
13170
|
-
|
|
13171
|
-
|
|
12697
|
+
this.logger('debug', 'Starting join flow');
|
|
12698
|
+
if (data?.ring && !this.ringing) {
|
|
12699
|
+
this.ringingSubject.next(true);
|
|
12700
|
+
}
|
|
12701
|
+
if (this.ringing && !this.isCreatedByMe) {
|
|
12702
|
+
// signals other users that I have accepted the incoming call.
|
|
12703
|
+
await this.accept();
|
|
12704
|
+
}
|
|
12705
|
+
let sfuServer;
|
|
12706
|
+
let sfuToken;
|
|
12707
|
+
let connectionConfig;
|
|
12708
|
+
let statsOptions;
|
|
12709
|
+
try {
|
|
12710
|
+
if (this.sfuClient?.isFastReconnecting) {
|
|
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;
|
|
13172
12716
|
}
|
|
13173
|
-
|
|
13174
|
-
//
|
|
13175
|
-
this.
|
|
13176
|
-
|
|
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;
|
|
13177
12727
|
}
|
|
13178
|
-
|
|
13179
|
-
|
|
13180
|
-
|
|
13181
|
-
const isWsHealthy = !!previousSfuClient?.isHealthy;
|
|
13182
|
-
const sfuClient = performingRejoin || performingMigration || !isWsHealthy
|
|
13183
|
-
? new StreamSfuClient({
|
|
13184
|
-
logTag: String(this.sfuClientTag++),
|
|
13185
|
-
dispatcher: this.dispatcher,
|
|
13186
|
-
credentials: this.credentials,
|
|
13187
|
-
// a new session_id is necessary for the REJOIN strategy.
|
|
13188
|
-
// we use the previous session_id if available
|
|
13189
|
-
sessionId: performingRejoin ? undefined : previousSessionId,
|
|
13190
|
-
onSignalClose: () => this.handleSfuSignalClose(sfuClient),
|
|
13191
|
-
})
|
|
13192
|
-
: previousSfuClient;
|
|
13193
|
-
this.sfuClient = sfuClient;
|
|
13194
|
-
const clientDetails = getClientDetails();
|
|
13195
|
-
// we don't need to send JoinRequest if we are re-using an existing healthy SFU client
|
|
13196
|
-
if (previousSfuClient !== sfuClient) {
|
|
13197
|
-
// prepare a generic SDP and send it to the SFU.
|
|
13198
|
-
// this is a throw-away SDP that the SFU will use to determine
|
|
13199
|
-
// the capabilities of the client (codec support, etc.)
|
|
13200
|
-
const receivingCapabilitiesSdp = await getGenericSdp('recvonly');
|
|
13201
|
-
const reconnectDetails = this.reconnectStrategy !== WebsocketReconnectStrategy.UNSPECIFIED
|
|
13202
|
-
? this.getReconnectDetails(data?.migrating_from, previousSessionId)
|
|
13203
|
-
: undefined;
|
|
13204
|
-
const { callState, fastReconnectDeadlineSeconds } = await sfuClient.join({
|
|
13205
|
-
subscriberSdp: receivingCapabilitiesSdp,
|
|
13206
|
-
clientDetails,
|
|
13207
|
-
fastReconnect: performingFastReconnect,
|
|
13208
|
-
reconnectDetails,
|
|
13209
|
-
});
|
|
13210
|
-
this.fastReconnectDeadlineSeconds = fastReconnectDeadlineSeconds;
|
|
13211
|
-
if (callState) {
|
|
13212
|
-
this.state.updateFromSfuCallState(callState, sfuClient.sessionId, reconnectDetails);
|
|
12728
|
+
if (this.streamClient._hasConnectionID()) {
|
|
12729
|
+
this.watching = true;
|
|
12730
|
+
this.clientStore.registerCall(this);
|
|
13213
12731
|
}
|
|
13214
12732
|
}
|
|
13215
|
-
|
|
13216
|
-
|
|
13217
|
-
|
|
13218
|
-
|
|
13219
|
-
if (performingFastReconnect) {
|
|
13220
|
-
// the SFU automatically issues an ICE restart on the subscriber
|
|
13221
|
-
// we don't have to do it ourselves
|
|
13222
|
-
await this.restoreICE(sfuClient, { includeSubscriber: false });
|
|
12733
|
+
catch (error) {
|
|
12734
|
+
// restore the previous call state if the join-flow fails
|
|
12735
|
+
this.state.setCallingState(callingState);
|
|
12736
|
+
throw error;
|
|
13223
12737
|
}
|
|
13224
|
-
|
|
13225
|
-
|
|
13226
|
-
this.
|
|
13227
|
-
|
|
13228
|
-
|
|
13229
|
-
|
|
13230
|
-
|
|
13231
|
-
|
|
12738
|
+
const previousSfuClient = this.sfuClient;
|
|
12739
|
+
const sfuClient = (this.sfuClient = new StreamSfuClient({
|
|
12740
|
+
dispatcher: this.dispatcher,
|
|
12741
|
+
sfuServer,
|
|
12742
|
+
token: sfuToken,
|
|
12743
|
+
sessionId: previousSfuClient?.sessionId,
|
|
12744
|
+
}));
|
|
12745
|
+
/**
|
|
12746
|
+
* A closure which hides away the re-connection logic.
|
|
12747
|
+
*/
|
|
12748
|
+
const reconnect = async (strategy, reason) => {
|
|
12749
|
+
const currentState = this.state.callingState;
|
|
12750
|
+
if (currentState === CallingState.MIGRATING ||
|
|
12751
|
+
currentState === CallingState.RECONNECTING) {
|
|
12752
|
+
// prevent parallel reconnection attempts
|
|
12753
|
+
return;
|
|
12754
|
+
}
|
|
12755
|
+
this.reconnectAttempts++;
|
|
12756
|
+
this.state.setCallingState(strategy === 'migrate'
|
|
12757
|
+
? CallingState.MIGRATING
|
|
12758
|
+
: CallingState.RECONNECTING);
|
|
12759
|
+
if (strategy === 'migrate') {
|
|
12760
|
+
this.logger('debug', `[Migration]: migrating call ${this.cid} away from ${sfuServer.edge_name}`);
|
|
12761
|
+
sfuClient.isMigratingAway = true;
|
|
12762
|
+
}
|
|
12763
|
+
else {
|
|
12764
|
+
this.logger('debug', `[Rejoin]: ${strategy} rejoin call ${this.cid} (${this.reconnectAttempts})...`);
|
|
12765
|
+
}
|
|
12766
|
+
// take a snapshot of the current "local participant" state
|
|
12767
|
+
// we'll need it for restoring the previous publishing state later
|
|
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 }),
|
|
13232
12791
|
});
|
|
13233
|
-
|
|
13234
|
-
|
|
13235
|
-
|
|
13236
|
-
await previousSfuClient?.leaveAndClose(`Closing previous WS after reconnect with strategy: ${strategy}`);
|
|
13237
|
-
}
|
|
13238
|
-
else if (!isWsHealthy) {
|
|
13239
|
-
previousSfuClient?.close(4002, 'Closing unhealthy WS after reconnect');
|
|
13240
|
-
}
|
|
13241
|
-
// device settings should be applied only once, we don't have to
|
|
13242
|
-
// re-apply them on later reconnections or server-side data fetches
|
|
13243
|
-
if (!this.deviceSettingsAppliedOnce) {
|
|
13244
|
-
await this.applyDeviceConfig(true);
|
|
13245
|
-
this.deviceSettingsAppliedOnce = true;
|
|
13246
|
-
}
|
|
13247
|
-
this.logger('info', `Joined call ${this.cid}`);
|
|
13248
|
-
};
|
|
13249
|
-
/**
|
|
13250
|
-
* Prepares Reconnect Details object.
|
|
13251
|
-
* @internal
|
|
13252
|
-
*/
|
|
13253
|
-
this.getReconnectDetails = (migratingFromSfuId, previousSessionId) => {
|
|
13254
|
-
const strategy = this.reconnectStrategy;
|
|
13255
|
-
const performingRejoin = strategy === WebsocketReconnectStrategy.REJOIN;
|
|
13256
|
-
const announcedTracks = this.publisher?.getAnnouncedTracks() || [];
|
|
13257
|
-
const subscribedTracks = getCurrentValue(this.trackSubscriptionsSubject);
|
|
13258
|
-
return {
|
|
13259
|
-
strategy,
|
|
13260
|
-
announcedTracks,
|
|
13261
|
-
subscriptions: subscribedTracks.data || [],
|
|
13262
|
-
reconnectAttempt: this.reconnectAttempts,
|
|
13263
|
-
fromSfuId: migratingFromSfuId || '',
|
|
13264
|
-
previousSessionId: performingRejoin ? previousSessionId || '' : '',
|
|
13265
|
-
};
|
|
13266
|
-
};
|
|
13267
|
-
/**
|
|
13268
|
-
* Performs an ICE restart on both the Publisher and Subscriber Peer Connections.
|
|
13269
|
-
* Uses the provided SFU client to restore the ICE connection.
|
|
13270
|
-
*
|
|
13271
|
-
* This method can throw an error if the ICE restart fails.
|
|
13272
|
-
* This error should be handled by the reconnect loop,
|
|
13273
|
-
* and a new reconnection shall be attempted.
|
|
13274
|
-
*
|
|
13275
|
-
* @internal
|
|
13276
|
-
*/
|
|
13277
|
-
this.restoreICE = async (nextSfuClient, opts = {}) => {
|
|
13278
|
-
const { includeSubscriber = true, includePublisher = true } = opts;
|
|
13279
|
-
if (this.subscriber) {
|
|
13280
|
-
this.subscriber.setSfuClient(nextSfuClient);
|
|
13281
|
-
if (includeSubscriber) {
|
|
13282
|
-
await this.subscriber.restartIce();
|
|
12792
|
+
// clean up previous connection
|
|
12793
|
+
if (strategy === 'migrate') {
|
|
12794
|
+
sfuClient.close(StreamSfuClient.NORMAL_CLOSURE, 'attempting migration');
|
|
13283
12795
|
}
|
|
13284
|
-
|
|
13285
|
-
|
|
13286
|
-
|
|
13287
|
-
|
|
13288
|
-
|
|
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}`);
|
|
13289
12823
|
}
|
|
13290
|
-
}
|
|
13291
|
-
|
|
13292
|
-
|
|
13293
|
-
|
|
13294
|
-
|
|
13295
|
-
|
|
13296
|
-
|
|
13297
|
-
|
|
13298
|
-
|
|
13299
|
-
|
|
13300
|
-
|
|
13301
|
-
this.subscriber = new Subscriber({
|
|
13302
|
-
sfuClient,
|
|
13303
|
-
dispatcher: this.dispatcher,
|
|
13304
|
-
state: this.state,
|
|
13305
|
-
connectionConfig,
|
|
13306
|
-
logTag: String(this.reconnectAttempts),
|
|
13307
|
-
onUnrecoverableError: () => {
|
|
13308
|
-
this.reconnect(WebsocketReconnectStrategy.REJOIN).catch((err) => {
|
|
13309
|
-
this.logger('warn', '[Reconnect] Error reconnecting after a subscriber error', err);
|
|
12824
|
+
};
|
|
12825
|
+
// reconnect if the connection was closed unexpectedly. example:
|
|
12826
|
+
// - SFU crash or restart
|
|
12827
|
+
// - network change
|
|
12828
|
+
sfuClient.signalReady.then(() => {
|
|
12829
|
+
// register a handler for the "goAway" event
|
|
12830
|
+
const unregisterGoAway = this.dispatcher.on('goAway', (event) => {
|
|
12831
|
+
const { reason } = event;
|
|
12832
|
+
this.logger('info', `[Migration]: Going away from SFU... Reason: ${GoAwayReason[reason]}`);
|
|
12833
|
+
reconnect('migrate', GoAwayReason[reason]).catch((err) => {
|
|
12834
|
+
this.logger('warn', `[Migration]: Failed to migrate to another SFU.`, err);
|
|
13310
12835
|
});
|
|
13311
|
-
}
|
|
12836
|
+
});
|
|
12837
|
+
sfuClient.signalWs.addEventListener('close', (e) => {
|
|
12838
|
+
// unregister the "goAway" handler, as we won't need it anymore for this connection.
|
|
12839
|
+
// the upcoming re-join will register a new handler anyway
|
|
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);
|
|
13312
12916
|
});
|
|
12917
|
+
this.leaveCallHooks.add(() => {
|
|
12918
|
+
unsubscribeOnlineEvent();
|
|
12919
|
+
unsubscribeOfflineEvent();
|
|
12920
|
+
});
|
|
12921
|
+
if (!this.subscriber) {
|
|
12922
|
+
this.subscriber = new Subscriber({
|
|
12923
|
+
sfuClient,
|
|
12924
|
+
dispatcher: this.dispatcher,
|
|
12925
|
+
state: this.state,
|
|
12926
|
+
connectionConfig,
|
|
12927
|
+
onUnrecoverableError: () => {
|
|
12928
|
+
reconnect('full', 'unrecoverable subscriber error').catch((err) => {
|
|
12929
|
+
this.logger('debug', '[Rejoin]: Rejoin failed', err);
|
|
12930
|
+
});
|
|
12931
|
+
},
|
|
12932
|
+
});
|
|
12933
|
+
}
|
|
13313
12934
|
// anonymous users can't publish anything hence, there is no need
|
|
13314
12935
|
// to create Publisher Peer Connection for them
|
|
13315
12936
|
const isAnonymous = this.streamClient.user?.type === 'anonymous';
|
|
13316
|
-
if (!isAnonymous) {
|
|
13317
|
-
if (closePreviousInstances && this.publisher) {
|
|
13318
|
-
this.publisher.close({ stopTracks: false });
|
|
13319
|
-
}
|
|
12937
|
+
if (!this.publisher && !isAnonymous) {
|
|
13320
12938
|
const audioSettings = this.state.settings?.audio;
|
|
13321
12939
|
const isDtxEnabled = !!audioSettings?.opus_dtx_enabled;
|
|
13322
12940
|
const isRedEnabled = !!audioSettings?.redundant_coding_enabled;
|
|
@@ -13327,23 +12945,23 @@ class Call {
|
|
|
13327
12945
|
connectionConfig,
|
|
13328
12946
|
isDtxEnabled,
|
|
13329
12947
|
isRedEnabled,
|
|
13330
|
-
logTag: String(this.reconnectAttempts),
|
|
13331
12948
|
onUnrecoverableError: () => {
|
|
13332
|
-
|
|
13333
|
-
this.logger('
|
|
12949
|
+
reconnect('full', 'unrecoverable publisher error').catch((err) => {
|
|
12950
|
+
this.logger('debug', '[Rejoin]: Rejoin failed', err);
|
|
13334
12951
|
});
|
|
13335
12952
|
},
|
|
13336
12953
|
});
|
|
13337
12954
|
}
|
|
13338
|
-
this.statsReporter
|
|
13339
|
-
|
|
13340
|
-
|
|
13341
|
-
|
|
13342
|
-
|
|
13343
|
-
|
|
13344
|
-
|
|
13345
|
-
|
|
13346
|
-
|
|
12955
|
+
if (!this.statsReporter) {
|
|
12956
|
+
this.statsReporter = createStatsReporter({
|
|
12957
|
+
subscriber: this.subscriber,
|
|
12958
|
+
publisher: this.publisher,
|
|
12959
|
+
state: this.state,
|
|
12960
|
+
datacenter: this.sfuClient.edgeName,
|
|
12961
|
+
});
|
|
12962
|
+
}
|
|
12963
|
+
const clientDetails = getClientDetails();
|
|
12964
|
+
if (!this.sfuStatsReporter && statsOptions) {
|
|
13347
12965
|
this.sfuStatsReporter = new SfuStatsReporter(sfuClient, {
|
|
13348
12966
|
clientDetails,
|
|
13349
12967
|
options: statsOptions,
|
|
@@ -13352,258 +12970,129 @@ class Call {
|
|
|
13352
12970
|
});
|
|
13353
12971
|
this.sfuStatsReporter.start();
|
|
13354
12972
|
}
|
|
13355
|
-
};
|
|
13356
|
-
/**
|
|
13357
|
-
* Retrieves credentials for joining the call.
|
|
13358
|
-
*
|
|
13359
|
-
* @internal
|
|
13360
|
-
*
|
|
13361
|
-
* @param data the join call data.
|
|
13362
|
-
*/
|
|
13363
|
-
this.doJoinRequest = async (data) => {
|
|
13364
|
-
const location = await this.streamClient.getLocationHint();
|
|
13365
|
-
const request = { ...data, location };
|
|
13366
|
-
const joinResponse = await this.streamClient.post(`${this.streamClientBasePath}/join`, request);
|
|
13367
|
-
this.state.updateFromCallResponse(joinResponse.call);
|
|
13368
|
-
this.state.setMembers(joinResponse.members);
|
|
13369
|
-
this.state.setOwnCapabilities(joinResponse.own_capabilities);
|
|
13370
|
-
if (data?.ring && !this.ringing) {
|
|
13371
|
-
this.ringingSubject.next(true);
|
|
13372
|
-
}
|
|
13373
|
-
if (this.ringing && !this.isCreatedByMe) {
|
|
13374
|
-
// signals other users that I have accepted the incoming call.
|
|
13375
|
-
await this.accept();
|
|
13376
|
-
}
|
|
13377
|
-
if (this.streamClient._hasConnectionID()) {
|
|
13378
|
-
this.watching = true;
|
|
13379
|
-
this.clientStore.registerCall(this);
|
|
13380
|
-
}
|
|
13381
|
-
return joinResponse;
|
|
13382
|
-
};
|
|
13383
|
-
/**
|
|
13384
|
-
* Handles the reconnection flow.
|
|
13385
|
-
*
|
|
13386
|
-
* @internal
|
|
13387
|
-
*
|
|
13388
|
-
* @param strategy the reconnection strategy to use.
|
|
13389
|
-
*/
|
|
13390
|
-
this.reconnect = async (strategy) => {
|
|
13391
|
-
return withoutConcurrency(this.reconnectConcurrencyTag, async () => {
|
|
13392
|
-
this.logger('info', `[Reconnect] Reconnecting with strategy ${WebsocketReconnectStrategy[strategy]}`);
|
|
13393
|
-
this.reconnectStrategy = strategy;
|
|
13394
|
-
do {
|
|
13395
|
-
const current = WebsocketReconnectStrategy[this.reconnectStrategy];
|
|
13396
|
-
try {
|
|
13397
|
-
// wait until the network is available
|
|
13398
|
-
await this.networkAvailableTask?.promise;
|
|
13399
|
-
switch (this.reconnectStrategy) {
|
|
13400
|
-
case WebsocketReconnectStrategy.UNSPECIFIED:
|
|
13401
|
-
case WebsocketReconnectStrategy.DISCONNECT:
|
|
13402
|
-
this.logger('debug', `[Reconnect] No-op strategy ${current}`);
|
|
13403
|
-
break;
|
|
13404
|
-
case WebsocketReconnectStrategy.FAST:
|
|
13405
|
-
await this.reconnectFast();
|
|
13406
|
-
break;
|
|
13407
|
-
case WebsocketReconnectStrategy.REJOIN:
|
|
13408
|
-
await this.reconnectRejoin();
|
|
13409
|
-
break;
|
|
13410
|
-
case WebsocketReconnectStrategy.MIGRATE:
|
|
13411
|
-
await this.reconnectMigrate();
|
|
13412
|
-
break;
|
|
13413
|
-
default:
|
|
13414
|
-
ensureExhausted(this.reconnectStrategy, 'Unknown reconnection strategy');
|
|
13415
|
-
break;
|
|
13416
|
-
}
|
|
13417
|
-
break; // do-while loop, reconnection worked, exit the loop
|
|
13418
|
-
}
|
|
13419
|
-
catch (error) {
|
|
13420
|
-
this.logger('warn', `[Reconnect] ${current}(${this.reconnectAttempts}) failed. Attempting with REJOIN`, error);
|
|
13421
|
-
await sleep(retryInterval(this.reconnectAttempts));
|
|
13422
|
-
this.reconnectStrategy = WebsocketReconnectStrategy.REJOIN;
|
|
13423
|
-
this.reconnectAttempts++;
|
|
13424
|
-
}
|
|
13425
|
-
} while (this.state.callingState !== CallingState.JOINED &&
|
|
13426
|
-
this.state.callingState !== CallingState.LEFT);
|
|
13427
|
-
});
|
|
13428
|
-
};
|
|
13429
|
-
/**
|
|
13430
|
-
* Initiates the reconnection flow with the "fast" strategy.
|
|
13431
|
-
* @internal
|
|
13432
|
-
*/
|
|
13433
|
-
this.reconnectFast = async () => {
|
|
13434
|
-
this.reconnectStrategy = WebsocketReconnectStrategy.FAST;
|
|
13435
|
-
this.state.setCallingState(CallingState.RECONNECTING);
|
|
13436
|
-
return this.join(this.joinCallData);
|
|
13437
|
-
};
|
|
13438
|
-
/**
|
|
13439
|
-
* Initiates the reconnection flow with the "rejoin" strategy.
|
|
13440
|
-
* @internal
|
|
13441
|
-
*/
|
|
13442
|
-
this.reconnectRejoin = async () => {
|
|
13443
|
-
this.reconnectStrategy = WebsocketReconnectStrategy.REJOIN;
|
|
13444
|
-
this.state.setCallingState(CallingState.RECONNECTING);
|
|
13445
|
-
await this.join(this.joinCallData);
|
|
13446
|
-
await this.restorePublishedTracks();
|
|
13447
|
-
this.restoreSubscribedTracks();
|
|
13448
|
-
};
|
|
13449
|
-
/**
|
|
13450
|
-
* Initiates the reconnection flow with the "migrate" strategy.
|
|
13451
|
-
* @internal
|
|
13452
|
-
*/
|
|
13453
|
-
this.reconnectMigrate = async () => {
|
|
13454
|
-
const currentSfuClient = this.sfuClient;
|
|
13455
|
-
if (!currentSfuClient) {
|
|
13456
|
-
throw new Error('Cannot migrate without an active SFU client');
|
|
13457
|
-
}
|
|
13458
|
-
this.reconnectStrategy = WebsocketReconnectStrategy.MIGRATE;
|
|
13459
|
-
this.state.setCallingState(CallingState.MIGRATING);
|
|
13460
|
-
const currentSubscriber = this.subscriber;
|
|
13461
|
-
const currentPublisher = this.publisher;
|
|
13462
|
-
currentSubscriber?.detachEventHandlers();
|
|
13463
|
-
currentPublisher?.detachEventHandlers();
|
|
13464
|
-
const migrationTask = currentSfuClient.enterMigration();
|
|
13465
|
-
try {
|
|
13466
|
-
const currentSfu = currentSfuClient.edgeName;
|
|
13467
|
-
await this.join({ ...this.joinCallData, migrating_from: currentSfu });
|
|
13468
|
-
}
|
|
13469
|
-
finally {
|
|
13470
|
-
// cleanup the migration_from field after the migration is complete or failed
|
|
13471
|
-
// as we don't want to keep dirty data in the join call data
|
|
13472
|
-
delete this.joinCallData?.migrating_from;
|
|
13473
|
-
}
|
|
13474
|
-
await this.restorePublishedTracks();
|
|
13475
|
-
this.restoreSubscribedTracks();
|
|
13476
12973
|
try {
|
|
13477
|
-
//
|
|
13478
|
-
|
|
13479
|
-
|
|
13480
|
-
|
|
13481
|
-
|
|
13482
|
-
|
|
13483
|
-
|
|
13484
|
-
|
|
13485
|
-
|
|
13486
|
-
|
|
13487
|
-
|
|
13488
|
-
|
|
13489
|
-
|
|
13490
|
-
|
|
13491
|
-
|
|
13492
|
-
|
|
13493
|
-
|
|
13494
|
-
|
|
13495
|
-
|
|
13496
|
-
|
|
13497
|
-
|
|
13498
|
-
this.logger('warn', '[Reconnect] Error reconnecting', err);
|
|
13499
|
-
});
|
|
13500
|
-
});
|
|
13501
|
-
// handles the "error" event, through which the SFU can request a reconnect
|
|
13502
|
-
const unregisterOnError = this.on('error', (e) => {
|
|
13503
|
-
const { reconnectStrategy: strategy } = e;
|
|
13504
|
-
if (strategy === WebsocketReconnectStrategy.UNSPECIFIED)
|
|
13505
|
-
return;
|
|
13506
|
-
if (strategy === WebsocketReconnectStrategy.DISCONNECT) {
|
|
13507
|
-
this.leave({ reason: 'SFU instructed to disconnect' }).catch((err) => {
|
|
13508
|
-
this.logger('warn', `Can't leave call after disconnect request`, err);
|
|
12974
|
+
// 1. wait for the signal server to be ready before sending "joinRequest"
|
|
12975
|
+
sfuClient.signalReady
|
|
12976
|
+
.catch((err) => this.logger('error', 'Signal ready failed', err))
|
|
12977
|
+
// prepare a generic SDP and send it to the SFU.
|
|
12978
|
+
// this is a throw-away SDP that the SFU will use to determine
|
|
12979
|
+
// the capabilities of the client (codec support, etc.)
|
|
12980
|
+
.then(() => getGenericSdp('recvonly'))
|
|
12981
|
+
.then((sdp) => {
|
|
12982
|
+
const subscriptions = getCurrentValue(this.trackSubscriptionsSubject);
|
|
12983
|
+
const migration = isMigrating
|
|
12984
|
+
? {
|
|
12985
|
+
fromSfuId: data?.migrating_from || '',
|
|
12986
|
+
subscriptions: subscriptions.data || [],
|
|
12987
|
+
announcedTracks: this.publisher?.getCurrentTrackInfos() || [],
|
|
12988
|
+
}
|
|
12989
|
+
: undefined;
|
|
12990
|
+
return sfuClient.join({
|
|
12991
|
+
subscriberSdp: sdp || '',
|
|
12992
|
+
clientDetails,
|
|
12993
|
+
migration,
|
|
12994
|
+
fastReconnect: previousSfuClient?.isFastReconnecting ?? false,
|
|
13509
12995
|
});
|
|
12996
|
+
});
|
|
12997
|
+
// 2. in parallel, wait for the SFU to send us the "joinResponse"
|
|
12998
|
+
// this will throw an error if the SFU rejects the join request or
|
|
12999
|
+
// fails to respond in time
|
|
13000
|
+
const { callState, reconnected } = await this.waitForJoinResponse();
|
|
13001
|
+
if (isReconnecting) {
|
|
13002
|
+
this.logger('debug', '[Rejoin] fast reconnected:', reconnected);
|
|
13510
13003
|
}
|
|
13511
|
-
|
|
13512
|
-
this.
|
|
13513
|
-
|
|
13514
|
-
});
|
|
13004
|
+
if (isMigrating) {
|
|
13005
|
+
await this.subscriber.migrateTo(sfuClient, connectionConfig);
|
|
13006
|
+
await this.publisher?.migrateTo(sfuClient, connectionConfig);
|
|
13515
13007
|
}
|
|
13516
|
-
|
|
13517
|
-
|
|
13518
|
-
|
|
13519
|
-
|
|
13520
|
-
|
|
13521
|
-
|
|
13522
|
-
|
|
13523
|
-
|
|
13524
|
-
|
|
13525
|
-
networkAvailableTask.promise.then(() => {
|
|
13526
|
-
let strategy = WebsocketReconnectStrategy.FAST;
|
|
13527
|
-
if (this.lastOfflineTimestamp) {
|
|
13528
|
-
const offline = (Date.now() - this.lastOfflineTimestamp) / 1000;
|
|
13529
|
-
if (offline > this.fastReconnectDeadlineSeconds) {
|
|
13530
|
-
// We shouldn't attempt FAST if we have exceeded the deadline.
|
|
13531
|
-
// The SFU would have already wiped out the session.
|
|
13532
|
-
strategy = WebsocketReconnectStrategy.REJOIN;
|
|
13533
|
-
}
|
|
13008
|
+
else if (isReconnecting) {
|
|
13009
|
+
if (reconnected) {
|
|
13010
|
+
// update the SFU client instance on the subscriber and publisher
|
|
13011
|
+
this.subscriber.setSfuClient(sfuClient);
|
|
13012
|
+
// publisher might not be there (anonymous users)
|
|
13013
|
+
if (this.publisher) {
|
|
13014
|
+
this.publisher.setSfuClient(sfuClient);
|
|
13015
|
+
// and perform a full ICE restart on the publisher
|
|
13016
|
+
await this.publisher.restartIce();
|
|
13534
13017
|
}
|
|
13535
|
-
|
|
13536
|
-
|
|
13018
|
+
}
|
|
13019
|
+
else if (previousSfuClient?.isFastReconnecting) {
|
|
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
|
+
},
|
|
13537
13045
|
});
|
|
13538
13046
|
});
|
|
13539
|
-
|
|
13540
|
-
|
|
13541
|
-
|
|
13047
|
+
});
|
|
13048
|
+
this.state.setParticipantCount(participantCount?.total || 0);
|
|
13049
|
+
this.state.setAnonymousParticipantCount(participantCount?.anonymous || 0);
|
|
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 });
|
|
13542
13057
|
}
|
|
13543
|
-
|
|
13544
|
-
this.logger('
|
|
13545
|
-
// TODO try to remove this .close call
|
|
13546
|
-
this.sfuClient?.close(4002, 'Closing WS to reconnect after going online');
|
|
13547
|
-
// we went online, release the previous waiters and reset the state
|
|
13548
|
-
this.networkAvailableTask?.resolve();
|
|
13549
|
-
this.networkAvailableTask = undefined;
|
|
13550
|
-
this.sfuStatsReporter?.start();
|
|
13058
|
+
catch (error) {
|
|
13059
|
+
this.logger('warn', 'Camera and/or mic init failed during join call', error);
|
|
13551
13060
|
}
|
|
13552
|
-
|
|
13553
|
-
|
|
13554
|
-
|
|
13555
|
-
|
|
13556
|
-
|
|
13557
|
-
|
|
13558
|
-
|
|
13559
|
-
|
|
13560
|
-
|
|
13561
|
-
|
|
13562
|
-
|
|
13563
|
-
|
|
13564
|
-
|
|
13565
|
-
|
|
13566
|
-
|
|
13567
|
-
|
|
13568
|
-
|
|
13569
|
-
|
|
13570
|
-
|
|
13571
|
-
|
|
13572
|
-
case TrackType.VIDEO:
|
|
13573
|
-
const videoStream = this.camera.state.mediaStream;
|
|
13574
|
-
if (videoStream) {
|
|
13575
|
-
await this.publishVideoStream(videoStream, {
|
|
13576
|
-
preferredCodec: this.camera.preferredCodec,
|
|
13577
|
-
});
|
|
13578
|
-
}
|
|
13579
|
-
break;
|
|
13580
|
-
case TrackType.SCREEN_SHARE:
|
|
13581
|
-
const screenShareStream = this.screenShare.state.mediaStream;
|
|
13582
|
-
if (screenShareStream) {
|
|
13583
|
-
await this.publishScreenShareStream(screenShareStream, {
|
|
13584
|
-
screenShareSettings: this.screenShare.getSettings(),
|
|
13585
|
-
});
|
|
13586
|
-
}
|
|
13587
|
-
break;
|
|
13588
|
-
// screen share audio can't exist without a screen share, so we handle it there
|
|
13589
|
-
case TrackType.SCREEN_SHARE_AUDIO:
|
|
13590
|
-
case TrackType.UNSPECIFIED:
|
|
13591
|
-
break;
|
|
13592
|
-
default:
|
|
13593
|
-
ensureExhausted(trackType, 'Unknown track type');
|
|
13594
|
-
break;
|
|
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!`);
|
|
13076
|
+
}
|
|
13077
|
+
else {
|
|
13078
|
+
this.logger('error', `[Rejoin]: Rejoin failed for ${this.reconnectAttempts} times. Giving up.`);
|
|
13079
|
+
this.state.setCallingState(CallingState.RECONNECTING_FAILED);
|
|
13080
|
+
throw new Error('Join failed');
|
|
13595
13081
|
}
|
|
13596
13082
|
}
|
|
13597
13083
|
};
|
|
13598
|
-
|
|
13599
|
-
|
|
13600
|
-
|
|
13601
|
-
|
|
13602
|
-
|
|
13603
|
-
|
|
13604
|
-
|
|
13605
|
-
|
|
13606
|
-
|
|
13084
|
+
this.waitForJoinResponse = (timeout = 5000) => {
|
|
13085
|
+
return new Promise((resolve, reject) => {
|
|
13086
|
+
const unsubscribe = this.on('joinResponse', (event) => {
|
|
13087
|
+
clearTimeout(timeoutId);
|
|
13088
|
+
unsubscribe();
|
|
13089
|
+
resolve(event);
|
|
13090
|
+
});
|
|
13091
|
+
const timeoutId = setTimeout(() => {
|
|
13092
|
+
unsubscribe();
|
|
13093
|
+
reject(new Error('Waiting for "joinResponse" has timed out'));
|
|
13094
|
+
}, timeout);
|
|
13095
|
+
});
|
|
13607
13096
|
};
|
|
13608
13097
|
/**
|
|
13609
13098
|
* Starts publishing the given video stream to the call.
|
|
@@ -13618,7 +13107,7 @@ class Call {
|
|
|
13618
13107
|
this.publishVideoStream = async (videoStream, opts = {}) => {
|
|
13619
13108
|
// we should wait until we get a JoinResponse from the SFU,
|
|
13620
13109
|
// otherwise we risk breaking the ICETrickle flow.
|
|
13621
|
-
await this.
|
|
13110
|
+
await this.assertCallJoined();
|
|
13622
13111
|
if (!this.publisher) {
|
|
13623
13112
|
this.logger('error', 'Trying to publish video before join is completed');
|
|
13624
13113
|
throw new Error(`Call not joined yet.`);
|
|
@@ -13628,9 +13117,6 @@ class Call {
|
|
|
13628
13117
|
this.logger('error', `There is no video track to publish in the stream.`);
|
|
13629
13118
|
return;
|
|
13630
13119
|
}
|
|
13631
|
-
if (!this.trackPublishOrder.includes(TrackType.VIDEO)) {
|
|
13632
|
-
this.trackPublishOrder.push(TrackType.VIDEO);
|
|
13633
|
-
}
|
|
13634
13120
|
await this.publisher.publishStream(videoStream, videoTrack, TrackType.VIDEO, opts);
|
|
13635
13121
|
};
|
|
13636
13122
|
/**
|
|
@@ -13645,7 +13131,7 @@ class Call {
|
|
|
13645
13131
|
this.publishAudioStream = async (audioStream) => {
|
|
13646
13132
|
// we should wait until we get a JoinResponse from the SFU,
|
|
13647
13133
|
// otherwise we risk breaking the ICETrickle flow.
|
|
13648
|
-
await this.
|
|
13134
|
+
await this.assertCallJoined();
|
|
13649
13135
|
if (!this.publisher) {
|
|
13650
13136
|
this.logger('error', 'Trying to publish audio before join is completed');
|
|
13651
13137
|
throw new Error(`Call not joined yet.`);
|
|
@@ -13655,9 +13141,6 @@ class Call {
|
|
|
13655
13141
|
this.logger('error', `There is no audio track in the stream to publish`);
|
|
13656
13142
|
return;
|
|
13657
13143
|
}
|
|
13658
|
-
if (!this.trackPublishOrder.includes(TrackType.AUDIO)) {
|
|
13659
|
-
this.trackPublishOrder.push(TrackType.AUDIO);
|
|
13660
|
-
}
|
|
13661
13144
|
await this.publisher.publishStream(audioStream, audioTrack, TrackType.AUDIO);
|
|
13662
13145
|
};
|
|
13663
13146
|
/**
|
|
@@ -13672,7 +13155,7 @@ class Call {
|
|
|
13672
13155
|
this.publishScreenShareStream = async (screenShareStream, opts = {}) => {
|
|
13673
13156
|
// we should wait until we get a JoinResponse from the SFU,
|
|
13674
13157
|
// otherwise we risk breaking the ICETrickle flow.
|
|
13675
|
-
await this.
|
|
13158
|
+
await this.assertCallJoined();
|
|
13676
13159
|
if (!this.publisher) {
|
|
13677
13160
|
this.logger('error', 'Trying to publish screen share before join is completed');
|
|
13678
13161
|
throw new Error(`Call not joined yet.`);
|
|
@@ -13682,15 +13165,9 @@ class Call {
|
|
|
13682
13165
|
this.logger('error', `There is no video track in the screen share stream to publish`);
|
|
13683
13166
|
return;
|
|
13684
13167
|
}
|
|
13685
|
-
if (!this.trackPublishOrder.includes(TrackType.SCREEN_SHARE)) {
|
|
13686
|
-
this.trackPublishOrder.push(TrackType.SCREEN_SHARE);
|
|
13687
|
-
}
|
|
13688
13168
|
await this.publisher.publishStream(screenShareStream, screenShareTrack, TrackType.SCREEN_SHARE, opts);
|
|
13689
13169
|
const [screenShareAudioTrack] = screenShareStream.getAudioTracks();
|
|
13690
13170
|
if (screenShareAudioTrack) {
|
|
13691
|
-
if (!this.trackPublishOrder.includes(TrackType.SCREEN_SHARE_AUDIO)) {
|
|
13692
|
-
this.trackPublishOrder.push(TrackType.SCREEN_SHARE_AUDIO);
|
|
13693
|
-
}
|
|
13694
13171
|
await this.publisher.publishStream(screenShareStream, screenShareAudioTrack, TrackType.SCREEN_SHARE_AUDIO, opts);
|
|
13695
13172
|
}
|
|
13696
13173
|
};
|
|
@@ -13735,9 +13212,19 @@ class Call {
|
|
|
13735
13212
|
* @param type the debounce type to use for the update.
|
|
13736
13213
|
*/
|
|
13737
13214
|
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
|
+
}
|
|
13738
13223
|
const participants = this.state.updateParticipants(Object.entries(changes).reduce((acc, [sessionId, change]) => {
|
|
13739
|
-
if (change.dimension) {
|
|
13224
|
+
if (change.dimension?.height) {
|
|
13740
13225
|
change.dimension.height = Math.ceil(change.dimension.height);
|
|
13226
|
+
}
|
|
13227
|
+
if (change.dimension?.width) {
|
|
13741
13228
|
change.dimension.width = Math.ceil(change.dimension.width);
|
|
13742
13229
|
}
|
|
13743
13230
|
const prop = trackType === 'videoTrack'
|
|
@@ -13752,7 +13239,9 @@ class Call {
|
|
|
13752
13239
|
}
|
|
13753
13240
|
return acc;
|
|
13754
13241
|
}, {}));
|
|
13755
|
-
|
|
13242
|
+
if (participants) {
|
|
13243
|
+
this.updateSubscriptions(participants, type);
|
|
13244
|
+
}
|
|
13756
13245
|
};
|
|
13757
13246
|
this.updateSubscriptions = (participants, type = DebounceType.SLOW) => {
|
|
13758
13247
|
const subscriptions = [];
|
|
@@ -13835,10 +13324,10 @@ class Call {
|
|
|
13835
13324
|
this.updatePublishQuality = async (enabledLayers) => {
|
|
13836
13325
|
return this.publisher?.updateVideoPublishQuality(enabledLayers);
|
|
13837
13326
|
};
|
|
13838
|
-
this.
|
|
13327
|
+
this.assertCallJoined = () => {
|
|
13839
13328
|
return new Promise((resolve) => {
|
|
13840
13329
|
this.state.callingState$
|
|
13841
|
-
.pipe(takeWhile((state) => state !== CallingState.JOINED, true), filter((
|
|
13330
|
+
.pipe(takeWhile((state) => state !== CallingState.JOINED, true), filter((s) => s === CallingState.JOINED))
|
|
13842
13331
|
.subscribe(() => resolve());
|
|
13843
13332
|
});
|
|
13844
13333
|
};
|
|
@@ -14222,72 +13711,14 @@ class Call {
|
|
|
14222
13711
|
*
|
|
14223
13712
|
* @internal
|
|
14224
13713
|
*/
|
|
14225
|
-
this.applyDeviceConfig = async (
|
|
14226
|
-
await this.initCamera({ setStatus:
|
|
13714
|
+
this.applyDeviceConfig = async () => {
|
|
13715
|
+
await this.initCamera({ setStatus: false }).catch((err) => {
|
|
14227
13716
|
this.logger('warn', 'Camera init failed', err);
|
|
14228
13717
|
});
|
|
14229
|
-
await this.initMic({ setStatus:
|
|
13718
|
+
await this.initMic({ setStatus: false }).catch((err) => {
|
|
14230
13719
|
this.logger('warn', 'Mic init failed', err);
|
|
14231
13720
|
});
|
|
14232
13721
|
};
|
|
14233
|
-
this.initCamera = async (options) => {
|
|
14234
|
-
// Wait for any in progress camera operation
|
|
14235
|
-
await this.camera.statusChangeSettled();
|
|
14236
|
-
if (this.state.localParticipant?.videoStream ||
|
|
14237
|
-
!this.permissionsContext.hasPermission('send-video')) {
|
|
14238
|
-
return;
|
|
14239
|
-
}
|
|
14240
|
-
// Set camera direction if it's not yet set
|
|
14241
|
-
if (!this.camera.state.direction && !this.camera.state.selectedDevice) {
|
|
14242
|
-
let defaultDirection = 'front';
|
|
14243
|
-
const backendSetting = this.state.settings?.video.camera_facing;
|
|
14244
|
-
if (backendSetting) {
|
|
14245
|
-
defaultDirection = backendSetting === 'front' ? 'front' : 'back';
|
|
14246
|
-
}
|
|
14247
|
-
this.camera.state.setDirection(defaultDirection);
|
|
14248
|
-
}
|
|
14249
|
-
// Set target resolution
|
|
14250
|
-
const targetResolution = this.state.settings?.video.target_resolution;
|
|
14251
|
-
if (targetResolution) {
|
|
14252
|
-
await this.camera.selectTargetResolution(targetResolution);
|
|
14253
|
-
}
|
|
14254
|
-
if (options.setStatus) {
|
|
14255
|
-
// Publish already that was set before we joined
|
|
14256
|
-
if (this.camera.enabled &&
|
|
14257
|
-
this.camera.state.mediaStream &&
|
|
14258
|
-
!this.publisher?.isPublishing(TrackType.VIDEO)) {
|
|
14259
|
-
await this.publishVideoStream(this.camera.state.mediaStream, {
|
|
14260
|
-
preferredCodec: this.camera.preferredCodec,
|
|
14261
|
-
});
|
|
14262
|
-
}
|
|
14263
|
-
// Start camera if backend config specifies, and there is no local setting
|
|
14264
|
-
if (this.camera.state.status === undefined &&
|
|
14265
|
-
this.state.settings?.video.camera_default_on) {
|
|
14266
|
-
await this.camera.enable();
|
|
14267
|
-
}
|
|
14268
|
-
}
|
|
14269
|
-
};
|
|
14270
|
-
this.initMic = async (options) => {
|
|
14271
|
-
// Wait for any in progress mic operation
|
|
14272
|
-
await this.microphone.statusChangeSettled();
|
|
14273
|
-
if (this.state.localParticipant?.audioStream ||
|
|
14274
|
-
!this.permissionsContext.hasPermission('send-audio')) {
|
|
14275
|
-
return;
|
|
14276
|
-
}
|
|
14277
|
-
if (options.setStatus) {
|
|
14278
|
-
// Publish media stream that was set before we joined
|
|
14279
|
-
if (this.microphone.enabled &&
|
|
14280
|
-
this.microphone.state.mediaStream &&
|
|
14281
|
-
!this.publisher?.isPublishing(TrackType.AUDIO)) {
|
|
14282
|
-
await this.publishAudioStream(this.microphone.state.mediaStream);
|
|
14283
|
-
}
|
|
14284
|
-
// Start mic if backend config specifies, and there is no local setting
|
|
14285
|
-
if (this.microphone.state.status === undefined &&
|
|
14286
|
-
this.state.settings?.audio.mic_default_on) {
|
|
14287
|
-
await this.microphone.enable();
|
|
14288
|
-
}
|
|
14289
|
-
}
|
|
14290
|
-
};
|
|
14291
13722
|
/**
|
|
14292
13723
|
* Will begin tracking the given element for visibility changes within the
|
|
14293
13724
|
* configured viewport element (`call.setViewport`).
|
|
@@ -14402,15 +13833,15 @@ class Call {
|
|
|
14402
13833
|
}
|
|
14403
13834
|
async setup() {
|
|
14404
13835
|
await withoutConcurrency(this.joinLeaveConcurrencyTag, async () => {
|
|
14405
|
-
if (this.initialized)
|
|
13836
|
+
if (this.initialized) {
|
|
14406
13837
|
return;
|
|
13838
|
+
}
|
|
14407
13839
|
this.leaveCallHooks.add(this.on('all', (event) => {
|
|
14408
13840
|
// update state with the latest event data
|
|
14409
13841
|
this.state.updateFromEvent(event);
|
|
14410
13842
|
}));
|
|
14411
|
-
this.leaveCallHooks.add(registerEventHandlers(this, this.dispatcher));
|
|
13843
|
+
this.leaveCallHooks.add(registerEventHandlers(this, this.state, this.dispatcher));
|
|
14412
13844
|
this.registerEffects();
|
|
14413
|
-
this.registerReconnectHandlers();
|
|
14414
13845
|
this.leaveCallHooks.add(createSubscription(this.trackSubscriptionsSubject.pipe(debounce((v) => timer(v.type)), map$1((v) => v.data)), (subscriptions) => this.sfuClient?.updateSubscriptions(subscriptions).catch((err) => {
|
|
14415
13846
|
this.logger('debug', `Failed to update track subscriptions`, err);
|
|
14416
13847
|
})));
|
|
@@ -14430,7 +13861,44 @@ class Call {
|
|
|
14430
13861
|
}));
|
|
14431
13862
|
this.leaveCallHooks.add(
|
|
14432
13863
|
// handle the case when the user permissions are modified.
|
|
14433
|
-
|
|
13864
|
+
createSubscription(this.state.ownCapabilities$, (ownCapabilities) => {
|
|
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
|
+
}));
|
|
14434
13902
|
this.leaveCallHooks.add(
|
|
14435
13903
|
// handles the case when the user is blocked by the call owner.
|
|
14436
13904
|
createSubscription(this.state.blockedUserIds$, async (blockedUserIds) => {
|
|
@@ -14522,20 +13990,63 @@ class Call {
|
|
|
14522
13990
|
get isCreatedByMe() {
|
|
14523
13991
|
return this.state.createdBy?.id === this.currentUserId;
|
|
14524
13992
|
}
|
|
14525
|
-
|
|
14526
|
-
|
|
14527
|
-
|
|
14528
|
-
|
|
14529
|
-
|
|
14530
|
-
*/
|
|
14531
|
-
handleSfuSignalClose(sfuClient) {
|
|
14532
|
-
this.logger('debug', '[Reconnect] SFU signal connection closed');
|
|
14533
|
-
// normal close, no need to reconnect
|
|
14534
|
-
if (sfuClient.isLeaving)
|
|
13993
|
+
async initCamera(options) {
|
|
13994
|
+
// Wait for any in progress camera operation
|
|
13995
|
+
await this.camera.statusChangeSettled();
|
|
13996
|
+
if (this.state.localParticipant?.videoStream ||
|
|
13997
|
+
!this.permissionsContext.hasPermission('send-video')) {
|
|
14535
13998
|
return;
|
|
14536
|
-
|
|
14537
|
-
|
|
14538
|
-
|
|
13999
|
+
}
|
|
14000
|
+
// Set camera direction if it's not yet set
|
|
14001
|
+
if (!this.camera.state.direction && !this.camera.state.selectedDevice) {
|
|
14002
|
+
let defaultDirection = 'front';
|
|
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')) {
|
|
14035
|
+
return;
|
|
14036
|
+
}
|
|
14037
|
+
if (options.setStatus) {
|
|
14038
|
+
// Publish media stream that was set before we joined
|
|
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
|
+
}
|
|
14539
14050
|
}
|
|
14540
14051
|
}
|
|
14541
14052
|
|
|
@@ -14740,7 +14251,6 @@ class StableWSConnection {
|
|
|
14740
14251
|
}
|
|
14741
14252
|
}
|
|
14742
14253
|
if (data) {
|
|
14743
|
-
data.received_at = new Date();
|
|
14744
14254
|
this.client.dispatchEvent(data);
|
|
14745
14255
|
}
|
|
14746
14256
|
this.scheduleConnectionCheck();
|
|
@@ -15081,7 +14591,7 @@ class StableWSConnection {
|
|
|
15081
14591
|
wsURL,
|
|
15082
14592
|
requestID: this.requestID,
|
|
15083
14593
|
});
|
|
15084
|
-
this.ws = new WebSocket
|
|
14594
|
+
this.ws = new WebSocket(wsURL);
|
|
15085
14595
|
this.ws.onopen = this.onopen.bind(this, this.wsID);
|
|
15086
14596
|
this.ws.onclose = this.onclose.bind(this, this.wsID);
|
|
15087
14597
|
this.ws.onerror = this.onerror.bind(this, this.wsID);
|
|
@@ -15367,7 +14877,7 @@ class TokenManager {
|
|
|
15367
14877
|
if (this.user && !this.token) {
|
|
15368
14878
|
return this.token;
|
|
15369
14879
|
}
|
|
15370
|
-
throw new Error(`
|
|
14880
|
+
throw new Error(`User token is not set. Either client.connectUser wasn't called or client.disconnect was called`);
|
|
15371
14881
|
};
|
|
15372
14882
|
this.isStatic = () => this.type === 'static';
|
|
15373
14883
|
this.loadTokenPromise = null;
|
|
@@ -15697,7 +15207,6 @@ class StreamClient {
|
|
|
15697
15207
|
const wsPromise = this.openConnection();
|
|
15698
15208
|
this.setUserPromise = Promise.all([setTokenPromise, wsPromise]).then((result) => result[1]);
|
|
15699
15209
|
try {
|
|
15700
|
-
addConnectionEventListeners(this.updateNetworkConnectionStatus);
|
|
15701
15210
|
return await this.setUserPromise;
|
|
15702
15211
|
}
|
|
15703
15212
|
catch (err) {
|
|
@@ -15793,7 +15302,6 @@ class StreamClient {
|
|
|
15793
15302
|
delete this.userID;
|
|
15794
15303
|
this.anonymous = false;
|
|
15795
15304
|
await this.closeConnection(timeout);
|
|
15796
|
-
removeConnectionEventListeners(this.updateNetworkConnectionStatus);
|
|
15797
15305
|
this.tokenManager.reset();
|
|
15798
15306
|
this.connectionIdPromise = undefined;
|
|
15799
15307
|
this.rejectConnectionId = undefined;
|
|
@@ -15813,7 +15321,6 @@ class StreamClient {
|
|
|
15813
15321
|
* connectAnonymousUser - Set an anonymous user and open a WebSocket connection
|
|
15814
15322
|
*/
|
|
15815
15323
|
this.connectAnonymousUser = async (user, tokenOrProvider) => {
|
|
15816
|
-
addConnectionEventListeners(this.updateNetworkConnectionStatus);
|
|
15817
15324
|
this.connectionIdPromise = new Promise((resolve, reject) => {
|
|
15818
15325
|
this.resolveConnectionId = resolve;
|
|
15819
15326
|
this.rejectConnectionId = reject;
|
|
@@ -15973,6 +15480,8 @@ class StreamClient {
|
|
|
15973
15480
|
return data;
|
|
15974
15481
|
};
|
|
15975
15482
|
this.dispatchEvent = (event) => {
|
|
15483
|
+
if (!event.received_at)
|
|
15484
|
+
event.received_at = new Date();
|
|
15976
15485
|
this.logger('debug', `Dispatching event: ${event.type}`, event);
|
|
15977
15486
|
if (!this.listeners)
|
|
15978
15487
|
return;
|
|
@@ -16063,7 +15572,7 @@ class StreamClient {
|
|
|
16063
15572
|
});
|
|
16064
15573
|
};
|
|
16065
15574
|
this.getUserAgent = () => {
|
|
16066
|
-
const version = "1.5.0
|
|
15575
|
+
const version = "1.5.0" ;
|
|
16067
15576
|
return (this.userAgent ||
|
|
16068
15577
|
`stream-video-javascript-client-${this.node ? 'node' : 'browser'}-${version}`);
|
|
16069
15578
|
};
|
|
@@ -16129,15 +15638,11 @@ class StreamClient {
|
|
|
16129
15638
|
client_request_id,
|
|
16130
15639
|
});
|
|
16131
15640
|
};
|
|
16132
|
-
|
|
16133
|
-
|
|
16134
|
-
|
|
16135
|
-
|
|
16136
|
-
|
|
16137
|
-
else if (event.type === 'online') {
|
|
16138
|
-
this.logger('debug', 'device went online');
|
|
16139
|
-
this.dispatchEvent({ type: 'network.changed', online: true });
|
|
16140
|
-
}
|
|
15641
|
+
/**
|
|
15642
|
+
* creates an abort controller that will be used by the next HTTP Request.
|
|
15643
|
+
*/
|
|
15644
|
+
this.createAbortControllerForNextRequest = () => {
|
|
15645
|
+
return (this.nextRequestAbortController = new AbortController());
|
|
16141
15646
|
};
|
|
16142
15647
|
// set the key
|
|
16143
15648
|
this.key = key;
|
|
@@ -16167,14 +15672,10 @@ class StreamClient {
|
|
|
16167
15672
|
});
|
|
16168
15673
|
}
|
|
16169
15674
|
this.setBaseURL(this.options.baseURL || 'https://video.stream-io-api.com/video');
|
|
16170
|
-
if (typeof process !== 'undefined' &&
|
|
16171
|
-
'env' in process &&
|
|
16172
|
-
process.env.STREAM_LOCAL_TEST_RUN) {
|
|
15675
|
+
if (typeof process !== 'undefined' && process.env.STREAM_LOCAL_TEST_RUN) {
|
|
16173
15676
|
this.setBaseURL('http://localhost:3030/video');
|
|
16174
15677
|
}
|
|
16175
|
-
if (typeof process !== 'undefined' &&
|
|
16176
|
-
'env' in process &&
|
|
16177
|
-
process.env.STREAM_LOCAL_TEST_HOST) {
|
|
15678
|
+
if (typeof process !== 'undefined' && process.env.STREAM_LOCAL_TEST_HOST) {
|
|
16178
15679
|
this.setBaseURL(`http://${process.env.STREAM_LOCAL_TEST_HOST}/video`);
|
|
16179
15680
|
}
|
|
16180
15681
|
this.axiosInstance = axios.create({
|
|
@@ -16209,99 +15710,6 @@ class StreamVideoClient {
|
|
|
16209
15710
|
constructor(apiKeyOrArgs, opts) {
|
|
16210
15711
|
this.logLevel = 'warn';
|
|
16211
15712
|
this.eventHandlersToUnregister = [];
|
|
16212
|
-
/**
|
|
16213
|
-
* Connects the given user to the client.
|
|
16214
|
-
* Only one user can connect at a time, if you want to change users, call `disconnectUser` before connecting a new user.
|
|
16215
|
-
* If the connection is successful, the connected user [state variable](#readonlystatestore) will be updated accordingly.
|
|
16216
|
-
*
|
|
16217
|
-
* @param user the user to connect.
|
|
16218
|
-
* @param token a token or a function that returns a token.
|
|
16219
|
-
*/
|
|
16220
|
-
this.connectUser = async (user, token) => {
|
|
16221
|
-
if (user.type === 'anonymous') {
|
|
16222
|
-
user.id = '!anon';
|
|
16223
|
-
return this.connectAnonymousUser(user, token);
|
|
16224
|
-
}
|
|
16225
|
-
let connectUser = () => {
|
|
16226
|
-
return this.streamClient.connectUser(user, token);
|
|
16227
|
-
};
|
|
16228
|
-
if (user.type === 'guest') {
|
|
16229
|
-
connectUser = async () => {
|
|
16230
|
-
return this.streamClient.connectGuestUser(user);
|
|
16231
|
-
};
|
|
16232
|
-
}
|
|
16233
|
-
this.connectionPromise = this.disconnectionPromise
|
|
16234
|
-
? this.disconnectionPromise.then(() => connectUser())
|
|
16235
|
-
: connectUser();
|
|
16236
|
-
this.connectionPromise?.finally(() => (this.connectionPromise = undefined));
|
|
16237
|
-
const connectUserResponse = await this.connectionPromise;
|
|
16238
|
-
// connectUserResponse will be void if connectUser called twice for the same user
|
|
16239
|
-
if (connectUserResponse?.me) {
|
|
16240
|
-
this.writeableStateStore.setConnectedUser(connectUserResponse.me);
|
|
16241
|
-
}
|
|
16242
|
-
this.eventHandlersToUnregister.push(this.on('connection.changed', (event) => {
|
|
16243
|
-
if (event.online) {
|
|
16244
|
-
const callsToReWatch = this.writeableStateStore.calls
|
|
16245
|
-
.filter((call) => call.watching)
|
|
16246
|
-
.map((call) => call.cid);
|
|
16247
|
-
this.logger('info', `Rewatching calls after connection changed ${callsToReWatch.join(', ')}`);
|
|
16248
|
-
if (callsToReWatch.length > 0) {
|
|
16249
|
-
this.queryCalls({
|
|
16250
|
-
watch: true,
|
|
16251
|
-
filter_conditions: {
|
|
16252
|
-
cid: { $in: callsToReWatch },
|
|
16253
|
-
},
|
|
16254
|
-
sort: [{ field: 'cid', direction: 1 }],
|
|
16255
|
-
}).catch((err) => {
|
|
16256
|
-
this.logger('error', 'Failed to re-watch calls', err);
|
|
16257
|
-
});
|
|
16258
|
-
}
|
|
16259
|
-
}
|
|
16260
|
-
}));
|
|
16261
|
-
this.eventHandlersToUnregister.push(this.on('call.created', (event) => {
|
|
16262
|
-
const { call, members } = event;
|
|
16263
|
-
if (user.id === call.created_by.id) {
|
|
16264
|
-
this.logger('warn', 'Received `call.created` sent by the current user');
|
|
16265
|
-
return;
|
|
16266
|
-
}
|
|
16267
|
-
this.logger('info', `New call created and registered: ${call.cid}`);
|
|
16268
|
-
const newCall = new Call({
|
|
16269
|
-
streamClient: this.streamClient,
|
|
16270
|
-
type: call.type,
|
|
16271
|
-
id: call.id,
|
|
16272
|
-
members,
|
|
16273
|
-
clientStore: this.writeableStateStore,
|
|
16274
|
-
});
|
|
16275
|
-
newCall.state.updateFromCallResponse(call);
|
|
16276
|
-
this.writeableStateStore.registerCall(newCall);
|
|
16277
|
-
}));
|
|
16278
|
-
this.eventHandlersToUnregister.push(this.on('call.ring', async (event) => {
|
|
16279
|
-
const { call, members } = event;
|
|
16280
|
-
if (user.id === call.created_by.id) {
|
|
16281
|
-
this.logger('debug', 'Received `call.ring` sent by the current user so ignoring the event');
|
|
16282
|
-
return;
|
|
16283
|
-
}
|
|
16284
|
-
// The call might already be tracked by the client,
|
|
16285
|
-
// if `call.created` was received before `call.ring`.
|
|
16286
|
-
// In that case, we cleanup the already tracked call.
|
|
16287
|
-
const prevCall = this.writeableStateStore.findCall(call.type, call.id);
|
|
16288
|
-
await prevCall?.leave({ reason: 'cleaning-up in call.ring' });
|
|
16289
|
-
// we create a new call
|
|
16290
|
-
const theCall = new Call({
|
|
16291
|
-
streamClient: this.streamClient,
|
|
16292
|
-
type: call.type,
|
|
16293
|
-
id: call.id,
|
|
16294
|
-
members,
|
|
16295
|
-
clientStore: this.writeableStateStore,
|
|
16296
|
-
ringing: true,
|
|
16297
|
-
});
|
|
16298
|
-
theCall.state.updateFromCallResponse(call);
|
|
16299
|
-
// we fetch the latest metadata for the call from the server
|
|
16300
|
-
await theCall.get();
|
|
16301
|
-
this.writeableStateStore.registerCall(theCall);
|
|
16302
|
-
}));
|
|
16303
|
-
return connectUserResponse;
|
|
16304
|
-
};
|
|
16305
15713
|
/**
|
|
16306
15714
|
* Disconnects the currently connected user from the client.
|
|
16307
15715
|
*
|
|
@@ -16314,12 +15722,16 @@ class StreamVideoClient {
|
|
|
16314
15722
|
if (!this.streamClient.user && !this.connectionPromise) {
|
|
16315
15723
|
return;
|
|
16316
15724
|
}
|
|
15725
|
+
const userId = this.streamClient.user?.id;
|
|
16317
15726
|
const disconnectUser = () => this.streamClient.disconnectUser(timeout);
|
|
16318
15727
|
this.disconnectionPromise = this.connectionPromise
|
|
16319
15728
|
? this.connectionPromise.then(() => disconnectUser())
|
|
16320
15729
|
: disconnectUser();
|
|
16321
15730
|
this.disconnectionPromise.finally(() => (this.disconnectionPromise = undefined));
|
|
16322
15731
|
await this.disconnectionPromise;
|
|
15732
|
+
if (userId) {
|
|
15733
|
+
StreamVideoClient._instanceMap.delete(userId);
|
|
15734
|
+
}
|
|
16323
15735
|
this.eventHandlersToUnregister.forEach((unregister) => unregister());
|
|
16324
15736
|
this.eventHandlersToUnregister = [];
|
|
16325
15737
|
this.writeableStateStore.setConnectedUser(undefined);
|
|
@@ -16386,7 +15798,7 @@ class StreamVideoClient {
|
|
|
16386
15798
|
clientStore: this.writeableStateStore,
|
|
16387
15799
|
});
|
|
16388
15800
|
call.state.updateFromCallResponse(c.call);
|
|
16389
|
-
await call.applyDeviceConfig(
|
|
15801
|
+
await call.applyDeviceConfig();
|
|
16390
15802
|
if (data.watch) {
|
|
16391
15803
|
this.writeableStateStore.registerCall(call);
|
|
16392
15804
|
}
|
|
@@ -16430,17 +15842,6 @@ class StreamVideoClient {
|
|
|
16430
15842
|
...(push_provider_name != null ? { push_provider_name } : {}),
|
|
16431
15843
|
});
|
|
16432
15844
|
};
|
|
16433
|
-
/**
|
|
16434
|
-
* addDevice - Adds a push device for a user.
|
|
16435
|
-
*
|
|
16436
|
-
* @param {string} id the device id
|
|
16437
|
-
* @param {string} push_provider the push provider name (eg. apn, firebase)
|
|
16438
|
-
* @param {string} push_provider_name user provided push provider name
|
|
16439
|
-
* @param {string} [userID] the user id (defaults to current user)
|
|
16440
|
-
*/
|
|
16441
|
-
this.addVoipDevice = async (id, push_provider, push_provider_name, userID) => {
|
|
16442
|
-
return await this.addDevice(id, push_provider, push_provider_name, userID, true);
|
|
16443
|
-
};
|
|
16444
15845
|
/**
|
|
16445
15846
|
* getDevices - Returns the devices associated with a current user
|
|
16446
15847
|
* @param {string} [userID] User ID. Only works on serverside
|
|
@@ -16468,7 +15869,7 @@ class StreamVideoClient {
|
|
|
16468
15869
|
this.onRingingCall = async (call_cid) => {
|
|
16469
15870
|
// if we find the call and is already ringing, we don't need to create a new call
|
|
16470
15871
|
// as client would have received the call.ring state because the app had WS alive when receiving push notifications
|
|
16471
|
-
let call = this.
|
|
15872
|
+
let call = this.readOnlyStateStore.calls.find((c) => c.cid === call_cid && c.ringing);
|
|
16472
15873
|
if (!call) {
|
|
16473
15874
|
// if not it means that WS is not alive when receiving the push notifications and we need to fetch the call
|
|
16474
15875
|
const [callType, callId] = call_cid.split(':');
|
|
@@ -16509,13 +15910,12 @@ class StreamVideoClient {
|
|
|
16509
15910
|
}
|
|
16510
15911
|
setLogger(logger, logLevel);
|
|
16511
15912
|
this.logger = getLogger(['client']);
|
|
16512
|
-
const coordinatorLogger = getLogger(['coordinator']);
|
|
16513
15913
|
if (typeof apiKeyOrArgs === 'string') {
|
|
16514
15914
|
this.streamClient = new StreamClient(apiKeyOrArgs, {
|
|
16515
15915
|
persistUserOnConnectionFailure: true,
|
|
16516
15916
|
...opts,
|
|
16517
15917
|
logLevel,
|
|
16518
|
-
logger:
|
|
15918
|
+
logger: this.logger,
|
|
16519
15919
|
});
|
|
16520
15920
|
}
|
|
16521
15921
|
else {
|
|
@@ -16523,14 +15923,12 @@ class StreamVideoClient {
|
|
|
16523
15923
|
persistUserOnConnectionFailure: true,
|
|
16524
15924
|
...apiKeyOrArgs.options,
|
|
16525
15925
|
logLevel,
|
|
16526
|
-
logger:
|
|
15926
|
+
logger: this.logger,
|
|
16527
15927
|
});
|
|
16528
15928
|
const sdkInfo = getSdkInfo();
|
|
16529
15929
|
if (sdkInfo) {
|
|
16530
|
-
|
|
16531
|
-
|
|
16532
|
-
const userAgent = this.streamClient.getUserAgent();
|
|
16533
|
-
this.streamClient.setUserAgent(`${userAgent}-video-${sdkName}-sdk-${sdkVersion}`);
|
|
15930
|
+
this.streamClient.setUserAgent(this.streamClient.getUserAgent() +
|
|
15931
|
+
`-video-${SdkType[sdkInfo.type].toLowerCase()}-sdk-${sdkInfo.major}.${sdkInfo.minor}.${sdkInfo.patch}`);
|
|
16534
15932
|
}
|
|
16535
15933
|
}
|
|
16536
15934
|
this.writeableStateStore = new StreamVideoWriteableStateStore();
|
|
@@ -16539,19 +15937,156 @@ class StreamVideoClient {
|
|
|
16539
15937
|
const user = apiKeyOrArgs.user;
|
|
16540
15938
|
const token = apiKeyOrArgs.token || apiKeyOrArgs.tokenProvider;
|
|
16541
15939
|
if (user) {
|
|
15940
|
+
let id = user.id;
|
|
15941
|
+
if (user.type === 'anonymous') {
|
|
15942
|
+
id = '!anon';
|
|
15943
|
+
}
|
|
15944
|
+
if (id) {
|
|
15945
|
+
if (StreamVideoClient._instanceMap.has(apiKeyOrArgs.apiKey + id)) {
|
|
15946
|
+
this.logger('warn', `A StreamVideoClient already exists for ${user.type === 'anonymous' ? 'an anyonymous user' : id}; Prefer using getOrCreateInstance method`);
|
|
15947
|
+
}
|
|
15948
|
+
user.id = id;
|
|
15949
|
+
StreamVideoClient._instanceMap.set(apiKeyOrArgs.apiKey + id, this);
|
|
15950
|
+
}
|
|
16542
15951
|
this.connectUser(user, token).catch((err) => {
|
|
16543
15952
|
this.logger('error', 'Failed to connect', err);
|
|
16544
15953
|
});
|
|
16545
15954
|
}
|
|
16546
15955
|
}
|
|
16547
15956
|
}
|
|
15957
|
+
static getOrCreateInstance(args) {
|
|
15958
|
+
const user = args.user;
|
|
15959
|
+
if (!user.id) {
|
|
15960
|
+
if (args.user.type === 'anonymous') {
|
|
15961
|
+
user.id = '!anon';
|
|
15962
|
+
}
|
|
15963
|
+
else {
|
|
15964
|
+
throw new Error('User ID is required for a non-anonymous user');
|
|
15965
|
+
}
|
|
15966
|
+
}
|
|
15967
|
+
if (!args.token && !args.tokenProvider) {
|
|
15968
|
+
if (args.user.type !== 'anonymous' && args.user.type !== 'guest') {
|
|
15969
|
+
throw new Error('TokenProvider or token is required for a user that is not a guest or anonymous');
|
|
15970
|
+
}
|
|
15971
|
+
}
|
|
15972
|
+
let instance = StreamVideoClient._instanceMap.get(args.apiKey + user.id);
|
|
15973
|
+
if (!instance) {
|
|
15974
|
+
instance = new StreamVideoClient({ ...args, user });
|
|
15975
|
+
}
|
|
15976
|
+
return instance;
|
|
15977
|
+
}
|
|
16548
15978
|
/**
|
|
16549
15979
|
* Return the reactive state store, use this if you want to be notified about changes to the client state
|
|
16550
15980
|
*/
|
|
16551
15981
|
get state() {
|
|
16552
15982
|
return this.readOnlyStateStore;
|
|
16553
15983
|
}
|
|
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
|
+
}
|
|
16554
16088
|
}
|
|
16089
|
+
StreamVideoClient._instanceMap = new Map();
|
|
16555
16090
|
|
|
16556
|
-
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,
|
|
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 };
|
|
16557
16092
|
//# sourceMappingURL=index.browser.es.js.map
|