@stream-io/video-client 0.0.1-alpha.83 → 0.0.1-alpha.85

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.
@@ -2,7 +2,6 @@ import 'webrtc-adapter';
2
2
  import { MessageType, isJsonObject, typeofJsonValue, reflectionMergePartial, UnknownFieldHandler, WireType, MESSAGE_TYPE, PbLong } from '@protobuf-ts/runtime';
3
3
  import { TwirpFetchTransport } from '@protobuf-ts/twirp-transport';
4
4
  import { ServiceType, stackIntercept } from '@protobuf-ts/runtime-rpc';
5
- import { v4 } from 'uuid';
6
5
  import { ReplaySubject, BehaviorSubject, Subject, takeWhile, debounceTime, startWith, pairwise, Observable, concatMap, from, shareReplay, merge, map as map$2, combineLatest, filter, firstValueFrom } from 'rxjs';
7
6
  import { take, combineLatestWith, map as map$1, distinctUntilChanged } from 'rxjs/operators';
8
7
  import axios, { AxiosHeaders } from 'axios';
@@ -3798,6 +3797,7 @@ class SignalServerClient {
3798
3797
  const defaultOptions = {
3799
3798
  baseUrl: '',
3800
3799
  sendJson: true,
3800
+ timeout: 5 * 1000,
3801
3801
  jsonOptions: {
3802
3802
  ignoreUnknownFields: true,
3803
3803
  },
@@ -3866,6 +3866,129 @@ class IceTrickleBuffer {
3866
3866
  }
3867
3867
  }
3868
3868
 
3869
+ const sleep = (m) => new Promise((r) => setTimeout(r, m));
3870
+ function isFunction(value) {
3871
+ return (value &&
3872
+ (Object.prototype.toString.call(value) === '[object Function]' ||
3873
+ 'function' === typeof value ||
3874
+ value instanceof Function));
3875
+ }
3876
+ // todo: rename so that it does not contain word "chat"
3877
+ const chatCodes = {
3878
+ TOKEN_EXPIRED: 40,
3879
+ WS_CLOSED_SUCCESS: 1000,
3880
+ };
3881
+ /**
3882
+ * retryInterval - A retry interval which increases acc to number of failures
3883
+ *
3884
+ * @return {number} Duration to wait in milliseconds
3885
+ */
3886
+ function retryInterval(numberOfFailures) {
3887
+ // try to reconnect in 0.25-5 seconds (random to spread out the load from failures)
3888
+ const max = Math.min(500 + numberOfFailures * 2000, 5000);
3889
+ const min = Math.min(Math.max(250, (numberOfFailures - 1) * 2000), 5000);
3890
+ return Math.floor(Math.random() * (max - min) + min);
3891
+ }
3892
+ function randomId() {
3893
+ return generateUUIDv4();
3894
+ }
3895
+ function hex(bytes) {
3896
+ let s = '';
3897
+ for (let i = 0; i < bytes.length; i++) {
3898
+ s += bytes[i].toString(16).padStart(2, '0');
3899
+ }
3900
+ return s;
3901
+ }
3902
+ // https://tools.ietf.org/html/rfc4122
3903
+ function generateUUIDv4() {
3904
+ const bytes = getRandomBytes(16);
3905
+ bytes[6] = (bytes[6] & 0x0f) | 0x40; // version
3906
+ bytes[8] = (bytes[8] & 0xbf) | 0x80; // variant
3907
+ return (hex(bytes.subarray(0, 4)) +
3908
+ '-' +
3909
+ hex(bytes.subarray(4, 6)) +
3910
+ '-' +
3911
+ hex(bytes.subarray(6, 8)) +
3912
+ '-' +
3913
+ hex(bytes.subarray(8, 10)) +
3914
+ '-' +
3915
+ hex(bytes.subarray(10, 16)));
3916
+ }
3917
+ function getRandomValuesWithMathRandom(bytes) {
3918
+ const max = Math.pow(2, (8 * bytes.byteLength) / bytes.length);
3919
+ for (let i = 0; i < bytes.length; i++) {
3920
+ bytes[i] = Math.random() * max;
3921
+ }
3922
+ }
3923
+ const getRandomValues = (() => {
3924
+ if (typeof crypto !== 'undefined' &&
3925
+ typeof (crypto === null || crypto === void 0 ? void 0 : crypto.getRandomValues) !== 'undefined') {
3926
+ return crypto.getRandomValues.bind(crypto);
3927
+ }
3928
+ else if (typeof msCrypto !== 'undefined') {
3929
+ return msCrypto.getRandomValues.bind(msCrypto);
3930
+ }
3931
+ else {
3932
+ return getRandomValuesWithMathRandom;
3933
+ }
3934
+ })();
3935
+ function getRandomBytes(length) {
3936
+ const bytes = new Uint8Array(length);
3937
+ getRandomValues(bytes);
3938
+ return bytes;
3939
+ }
3940
+ function convertErrorToJson(err) {
3941
+ const jsonObj = {};
3942
+ if (!err)
3943
+ return jsonObj;
3944
+ try {
3945
+ Object.getOwnPropertyNames(err).forEach((key) => {
3946
+ jsonObj[key] = Object.getOwnPropertyDescriptor(err, key);
3947
+ });
3948
+ }
3949
+ catch (_) {
3950
+ return {
3951
+ error: 'failed to serialize the error',
3952
+ };
3953
+ }
3954
+ return jsonObj;
3955
+ }
3956
+ /**
3957
+ * isOnline safely return the navigator.online value for browser env
3958
+ * if navigator is not in global object, it always return true
3959
+ */
3960
+ function isOnline() {
3961
+ const nav = typeof navigator !== 'undefined'
3962
+ ? navigator
3963
+ : typeof window !== 'undefined' && window.navigator
3964
+ ? window.navigator
3965
+ : undefined;
3966
+ if (!nav) {
3967
+ console.warn('isOnline failed to access window.navigator and assume browser is online');
3968
+ return true;
3969
+ }
3970
+ // RN navigator has undefined for onLine
3971
+ if (typeof nav.onLine !== 'boolean') {
3972
+ return true;
3973
+ }
3974
+ return nav.onLine;
3975
+ }
3976
+ /**
3977
+ * listenForConnectionChanges - Adds an event listener fired on browser going online or offline
3978
+ */
3979
+ function addConnectionEventListeners(cb) {
3980
+ if (typeof window !== 'undefined' && window.addEventListener) {
3981
+ window.addEventListener('offline', cb);
3982
+ window.addEventListener('online', cb);
3983
+ }
3984
+ }
3985
+ function removeConnectionEventListeners(cb) {
3986
+ if (typeof window !== 'undefined' && window.removeEventListener) {
3987
+ window.removeEventListener('offline', cb);
3988
+ window.removeEventListener('online', cb);
3989
+ }
3990
+ }
3991
+
3869
3992
  const hostnameFromUrl = (url) => {
3870
3993
  try {
3871
3994
  const u = new URL(url);
@@ -3894,6 +4017,13 @@ const toURL = (url) => {
3894
4017
  * The client used for exchanging information with the SFU.
3895
4018
  */
3896
4019
  class StreamSfuClient {
4020
+ /**
4021
+ * Constructs a new SFU client.
4022
+ *
4023
+ * @param dispatcher the event dispatcher to use.
4024
+ * @param url the URL of the SFU.
4025
+ * @param token the JWT token to use for authentication.
4026
+ */
3897
4027
  constructor(dispatcher, url, token) {
3898
4028
  /**
3899
4029
  * A buffer for ICE Candidates that are received before
@@ -3909,19 +4039,19 @@ class StreamSfuClient {
3909
4039
  clearTimeout(this.connectionCheckTimeout);
3910
4040
  };
3911
4041
  this.updateSubscriptions = (subscriptions) => __awaiter(this, void 0, void 0, function* () {
3912
- return this.rpc.updateSubscriptions({
4042
+ return retryable(() => this.rpc.updateSubscriptions({
3913
4043
  sessionId: this.sessionId,
3914
4044
  tracks: subscriptions,
3915
- });
4045
+ }));
3916
4046
  });
3917
4047
  this.setPublisher = (data) => __awaiter(this, void 0, void 0, function* () {
3918
- return this.rpc.setPublisher(Object.assign(Object.assign({}, data), { sessionId: this.sessionId }));
4048
+ return retryable(() => this.rpc.setPublisher(Object.assign(Object.assign({}, data), { sessionId: this.sessionId })));
3919
4049
  });
3920
4050
  this.sendAnswer = (data) => __awaiter(this, void 0, void 0, function* () {
3921
- return this.rpc.sendAnswer(Object.assign(Object.assign({}, data), { sessionId: this.sessionId }));
4051
+ return retryable(() => this.rpc.sendAnswer(Object.assign(Object.assign({}, data), { sessionId: this.sessionId })));
3922
4052
  });
3923
4053
  this.iceTrickle = (data) => __awaiter(this, void 0, void 0, function* () {
3924
- return this.rpc.iceTrickle(Object.assign(Object.assign({}, data), { sessionId: this.sessionId }));
4054
+ return retryable(() => this.rpc.iceTrickle(Object.assign(Object.assign({}, data), { sessionId: this.sessionId })));
3925
4055
  });
3926
4056
  this.updateMuteState = (trackType, muted) => __awaiter(this, void 0, void 0, function* () {
3927
4057
  return this.updateMuteStates({
@@ -3934,7 +4064,7 @@ class StreamSfuClient {
3934
4064
  });
3935
4065
  });
3936
4066
  this.updateMuteStates = (data) => __awaiter(this, void 0, void 0, function* () {
3937
- return this.rpc.updateMuteStates(Object.assign(Object.assign({}, data), { sessionId: this.sessionId }));
4067
+ return retryable(() => this.rpc.updateMuteStates(Object.assign(Object.assign({}, data), { sessionId: this.sessionId })));
3938
4068
  });
3939
4069
  this.join = (data) => __awaiter(this, void 0, void 0, function* () {
3940
4070
  const joinRequest = JoinRequest.create(Object.assign(Object.assign({}, data), { sessionId: this.sessionId, token: this.token }));
@@ -3979,7 +4109,7 @@ class StreamSfuClient {
3979
4109
  }
3980
4110
  }, this.unhealthyTimeoutInMs);
3981
4111
  };
3982
- this.sessionId = v4();
4112
+ this.sessionId = generateUUIDv4();
3983
4113
  this.token = token;
3984
4114
  this.rpc = createSignalClient({
3985
4115
  baseUrl: url,
@@ -4027,6 +4157,37 @@ class StreamSfuClient {
4027
4157
  });
4028
4158
  }
4029
4159
  }
4160
+ const MAX_RETRIES = 5;
4161
+ /**
4162
+ * Creates a closure which wraps the given RPC call and retries invoking
4163
+ * the RPC until it succeeds or the maximum number of retries is reached.
4164
+ *
4165
+ * Between each retry, there would be a random delay in order to avoid
4166
+ * request bursts towards the SFU.
4167
+ *
4168
+ * @param rpc the closure around the RPC call to execute.
4169
+ * @param <I> the type of the request object.
4170
+ * @param <O> the type of the response object.
4171
+ */
4172
+ const retryable = (rpc) => __awaiter(void 0, void 0, void 0, function* () {
4173
+ var _a;
4174
+ let retryAttempt = 0;
4175
+ let rpcCallResult;
4176
+ do {
4177
+ // don't delay the first invocation
4178
+ if (retryAttempt > 0) {
4179
+ yield sleep(retryInterval(retryAttempt));
4180
+ }
4181
+ rpcCallResult = yield rpc();
4182
+ // if the RPC call failed, log the error and retry
4183
+ if (rpcCallResult.response.error) {
4184
+ console.error('SFU Error:', rpcCallResult.response.error);
4185
+ }
4186
+ retryAttempt++;
4187
+ } while (((_a = rpcCallResult.response.error) === null || _a === void 0 ? void 0 : _a.shouldRetry) &&
4188
+ retryAttempt < MAX_RETRIES);
4189
+ return rpcCallResult;
4190
+ });
4030
4191
 
4031
4192
  function getIceCandidate(candidate) {
4032
4193
  if (!candidate.usernameFragment) {
@@ -5727,129 +5888,6 @@ const CallTypes = new CallTypesRegistry([
5727
5888
  }),
5728
5889
  ]);
5729
5890
 
5730
- const sleep = (m) => new Promise((r) => setTimeout(r, m));
5731
- function isFunction(value) {
5732
- return (value &&
5733
- (Object.prototype.toString.call(value) === '[object Function]' ||
5734
- 'function' === typeof value ||
5735
- value instanceof Function));
5736
- }
5737
- // todo: rename so that it does not contain word "chat"
5738
- const chatCodes = {
5739
- TOKEN_EXPIRED: 40,
5740
- WS_CLOSED_SUCCESS: 1000,
5741
- };
5742
- /**
5743
- * retryInterval - A retry interval which increases acc to number of failures
5744
- *
5745
- * @return {number} Duration to wait in milliseconds
5746
- */
5747
- function retryInterval(numberOfFailures) {
5748
- // try to reconnect in 0.25-5 seconds (random to spread out the load from failures)
5749
- const max = Math.min(500 + numberOfFailures * 2000, 5000);
5750
- const min = Math.min(Math.max(250, (numberOfFailures - 1) * 2000), 5000);
5751
- return Math.floor(Math.random() * (max - min) + min);
5752
- }
5753
- function randomId() {
5754
- return generateUUIDv4();
5755
- }
5756
- function hex(bytes) {
5757
- let s = '';
5758
- for (let i = 0; i < bytes.length; i++) {
5759
- s += bytes[i].toString(16).padStart(2, '0');
5760
- }
5761
- return s;
5762
- }
5763
- // https://tools.ietf.org/html/rfc4122
5764
- function generateUUIDv4() {
5765
- const bytes = getRandomBytes(16);
5766
- bytes[6] = (bytes[6] & 0x0f) | 0x40; // version
5767
- bytes[8] = (bytes[8] & 0xbf) | 0x80; // variant
5768
- return (hex(bytes.subarray(0, 4)) +
5769
- '-' +
5770
- hex(bytes.subarray(4, 6)) +
5771
- '-' +
5772
- hex(bytes.subarray(6, 8)) +
5773
- '-' +
5774
- hex(bytes.subarray(8, 10)) +
5775
- '-' +
5776
- hex(bytes.subarray(10, 16)));
5777
- }
5778
- function getRandomValuesWithMathRandom(bytes) {
5779
- const max = Math.pow(2, (8 * bytes.byteLength) / bytes.length);
5780
- for (let i = 0; i < bytes.length; i++) {
5781
- bytes[i] = Math.random() * max;
5782
- }
5783
- }
5784
- const getRandomValues = (() => {
5785
- if (typeof crypto !== 'undefined' &&
5786
- typeof (crypto === null || crypto === void 0 ? void 0 : crypto.getRandomValues) !== 'undefined') {
5787
- return crypto.getRandomValues.bind(crypto);
5788
- }
5789
- else if (typeof msCrypto !== 'undefined') {
5790
- return msCrypto.getRandomValues.bind(msCrypto);
5791
- }
5792
- else {
5793
- return getRandomValuesWithMathRandom;
5794
- }
5795
- })();
5796
- function getRandomBytes(length) {
5797
- const bytes = new Uint8Array(length);
5798
- getRandomValues(bytes);
5799
- return bytes;
5800
- }
5801
- function convertErrorToJson(err) {
5802
- const jsonObj = {};
5803
- if (!err)
5804
- return jsonObj;
5805
- try {
5806
- Object.getOwnPropertyNames(err).forEach((key) => {
5807
- jsonObj[key] = Object.getOwnPropertyDescriptor(err, key);
5808
- });
5809
- }
5810
- catch (_) {
5811
- return {
5812
- error: 'failed to serialize the error',
5813
- };
5814
- }
5815
- return jsonObj;
5816
- }
5817
- /**
5818
- * isOnline safely return the navigator.online value for browser env
5819
- * if navigator is not in global object, it always return true
5820
- */
5821
- function isOnline() {
5822
- const nav = typeof navigator !== 'undefined'
5823
- ? navigator
5824
- : typeof window !== 'undefined' && window.navigator
5825
- ? window.navigator
5826
- : undefined;
5827
- if (!nav) {
5828
- console.warn('isOnline failed to access window.navigator and assume browser is online');
5829
- return true;
5830
- }
5831
- // RN navigator has undefined for onLine
5832
- if (typeof nav.onLine !== 'boolean') {
5833
- return true;
5834
- }
5835
- return nav.onLine;
5836
- }
5837
- /**
5838
- * listenForConnectionChanges - Adds an event listener fired on browser going online or offline
5839
- */
5840
- function addConnectionEventListeners(cb) {
5841
- if (typeof window !== 'undefined' && window.addEventListener) {
5842
- window.addEventListener('offline', cb);
5843
- window.addEventListener('online', cb);
5844
- }
5845
- }
5846
- function removeConnectionEventListeners(cb) {
5847
- if (typeof window !== 'undefined' && window.removeEventListener) {
5848
- window.removeEventListener('offline', cb);
5849
- window.removeEventListener('online', cb);
5850
- }
5851
- }
5852
-
5853
5891
  const UPDATE_SUBSCRIPTIONS_DEBOUNCE_DURATION = 600;
5854
5892
  /**
5855
5893
  * A `Call` object represents the active call the user is part of.
@@ -5887,6 +5925,13 @@ class Call {
5887
5925
  this.dispatcher = new Dispatcher();
5888
5926
  this.trackSubscriptionsSubject = new Subject();
5889
5927
  this.reconnectAttempts = 0;
5928
+ this.maxReconnectAttempts = 10;
5929
+ /**
5930
+ * A list hooks/functions to invoke when the call is left.
5931
+ * A typical use case is to clean up some global event handlers.
5932
+ * @private
5933
+ */
5934
+ this.leaveCallHooks = [];
5890
5935
  /**
5891
5936
  * Remove subscription for WebSocket events that were created by the `on` method.
5892
5937
  * @param eventName
@@ -5913,6 +5958,8 @@ class Call {
5913
5958
  (_d = this.sfuClient) === null || _d === void 0 ? void 0 : _d.close();
5914
5959
  this.sfuClient = undefined;
5915
5960
  this.dispatcher.offAll();
5961
+ // Call all leave call hooks, e.g. to clean up global event handlers
5962
+ this.leaveCallHooks.forEach((hook) => hook());
5916
5963
  this.clientStore.setCurrentValue(this.clientStore.activeCallSubject, undefined);
5917
5964
  this.state.setCurrentValue(this.state.callingStateSubject, CallingState.LEFT);
5918
5965
  };
@@ -6004,12 +6051,15 @@ class Call {
6004
6051
  // do nothing if the connection was closed on purpose
6005
6052
  if (e.code === 1000)
6006
6053
  return;
6007
- if (this.reconnectAttempts >= 10) {
6008
- console.log('Reconnect attempts exceeded. Giving up...');
6009
- this.state.setCurrentValue(this.state.callingStateSubject, CallingState.RECONNECTING_FAILED);
6054
+ if (this.reconnectAttempts < this.maxReconnectAttempts) {
6055
+ rejoin().catch(() => {
6056
+ console.log(`Rejoin failed for ${this.reconnectAttempts} times. Giving up.`);
6057
+ this.state.setCurrentValue(this.state.callingStateSubject, CallingState.RECONNECTING_FAILED);
6058
+ });
6010
6059
  }
6011
6060
  else {
6012
- void rejoin();
6061
+ console.log('Reconnect attempts exceeded. Giving up...');
6062
+ this.state.setCurrentValue(this.state.callingStateSubject, CallingState.RECONNECTING_FAILED);
6013
6063
  }
6014
6064
  });
6015
6065
  });
@@ -6026,11 +6076,16 @@ class Call {
6026
6076
  if (this.state.getCurrentValue(this.state.callingState$) ===
6027
6077
  CallingState.OFFLINE) {
6028
6078
  console.log('Join: Going online...');
6029
- rejoin();
6079
+ rejoin().catch(() => {
6080
+ console.log(`Rejoin failed for ${this.reconnectAttempts} times. Giving up.`);
6081
+ this.state.setCurrentValue(this.state.callingStateSubject, CallingState.RECONNECTING_FAILED);
6082
+ });
6030
6083
  }
6031
6084
  };
6032
6085
  window.addEventListener('offline', handleOnOffline);
6033
6086
  window.addEventListener('online', handleOnOnline);
6087
+ // register cleanup hooks
6088
+ this.leaveCallHooks.push(() => window.removeEventListener('offline', handleOnOffline), () => window.removeEventListener('online', handleOnOnline));
6034
6089
  }
6035
6090
  this.subscriber = createSubscriber({
6036
6091
  rpcClient: sfuClient,
@@ -6088,7 +6143,7 @@ class Call {
6088
6143
  }
6089
6144
  catch (err) {
6090
6145
  // join failed, try to rejoin
6091
- if (this.reconnectAttempts < 10) {
6146
+ if (this.reconnectAttempts < this.maxReconnectAttempts) {
6092
6147
  yield rejoin();
6093
6148
  console.log(`Rejoin ${this.reconnectAttempts} successful!`);
6094
6149
  }
@@ -6234,12 +6289,10 @@ class Call {
6234
6289
  * @param trackType the track type to stop publishing.
6235
6290
  */
6236
6291
  this.stopPublish = (trackType) => __awaiter(this, void 0, void 0, function* () {
6237
- if (!this.publisher) {
6238
- throw new Error(`Call not joined yet.`);
6239
- }
6292
+ var _f;
6240
6293
  console.log(`stopPublish`, TrackType[trackType]);
6241
- const wasPublishing = this.publisher.unpublishStream(trackType);
6242
- if (wasPublishing) {
6294
+ const wasPublishing = (_f = this.publisher) === null || _f === void 0 ? void 0 : _f.unpublishStream(trackType);
6295
+ if (wasPublishing && this.sfuClient) {
6243
6296
  yield this.sfuClient.updateMuteState(trackType, true);
6244
6297
  const audioOrVideoOrScreenShareStream = trackTypeToParticipantStreamKey(trackType);
6245
6298
  if (audioOrVideoOrScreenShareStream) {
@@ -6316,8 +6369,8 @@ class Call {
6316
6369
  * @returns
6317
6370
  */
6318
6371
  this.getStats = (kind, selector) => __awaiter(this, void 0, void 0, function* () {
6319
- var _f;
6320
- return (_f = this.statsReporter) === null || _f === void 0 ? void 0 : _f.getRawStatsForTrack(kind, selector);
6372
+ var _g;
6373
+ return (_g = this.statsReporter) === null || _g === void 0 ? void 0 : _g.getRawStatsForTrack(kind, selector);
6321
6374
  });
6322
6375
  /**
6323
6376
  * Will enhance the reported stats with additional participant-specific information (`callStatsReport$` state [store variable](./StreamVideoClient.md/#readonlystatestore)).
@@ -6405,8 +6458,8 @@ class Call {
6405
6458
  * @returns
6406
6459
  */
6407
6460
  this.updatePublishQuality = (enabledRids) => __awaiter(this, void 0, void 0, function* () {
6408
- var _g;
6409
- return (_g = this.publisher) === null || _g === void 0 ? void 0 : _g.updateVideoPublishQuality(enabledRids);
6461
+ var _h;
6462
+ return (_h = this.publisher) === null || _h === void 0 ? void 0 : _h.updateVideoPublishQuality(enabledRids);
6410
6463
  });
6411
6464
  this.handleOnTrack = (e) => {
6412
6465
  const [primaryStream] = e.streams;
@@ -8728,7 +8781,7 @@ class StreamClient {
8728
8781
  }
8729
8782
  getUserAgent() {
8730
8783
  return (this.userAgent ||
8731
- `stream-video-javascript-client-${this.node ? 'node' : 'browser'}-${"0.0.1-alpha.82"}`);
8784
+ `stream-video-javascript-client-${this.node ? 'node' : 'browser'}-${"0.0.1-alpha.84"}`);
8732
8785
  }
8733
8786
  setUserAgent(userAgent) {
8734
8787
  this.userAgent = userAgent;