livekit-client 2.9.1 → 2.9.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. package/dist/livekit-client.esm.mjs +379 -65
  2. package/dist/livekit-client.esm.mjs.map +1 -1
  3. package/dist/livekit-client.umd.js +1 -1
  4. package/dist/livekit-client.umd.js.map +1 -1
  5. package/dist/src/connectionHelper/ConnectionCheck.d.ts +2 -0
  6. package/dist/src/connectionHelper/ConnectionCheck.d.ts.map +1 -1
  7. package/dist/src/connectionHelper/checks/Checker.d.ts +5 -2
  8. package/dist/src/connectionHelper/checks/Checker.d.ts.map +1 -1
  9. package/dist/src/connectionHelper/checks/cloudRegion.d.ts +17 -0
  10. package/dist/src/connectionHelper/checks/cloudRegion.d.ts.map +1 -0
  11. package/dist/src/connectionHelper/checks/connectionProtocol.d.ts +19 -0
  12. package/dist/src/connectionHelper/checks/connectionProtocol.d.ts.map +1 -0
  13. package/dist/src/connectionHelper/checks/publishAudio.d.ts.map +1 -1
  14. package/dist/src/connectionHelper/checks/publishVideo.d.ts +1 -0
  15. package/dist/src/connectionHelper/checks/publishVideo.d.ts.map +1 -1
  16. package/dist/src/index.d.ts +2 -2
  17. package/dist/src/index.d.ts.map +1 -1
  18. package/dist/src/room/StreamReader.d.ts +3 -3
  19. package/dist/src/room/StreamReader.d.ts.map +1 -1
  20. package/dist/src/room/StreamWriter.d.ts +3 -3
  21. package/dist/src/room/StreamWriter.d.ts.map +1 -1
  22. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  23. package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
  24. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  25. package/dist/src/room/track/create.d.ts.map +1 -1
  26. package/dist/src/room/types.d.ts +0 -5
  27. package/dist/src/room/types.d.ts.map +1 -1
  28. package/dist/src/room/utils.d.ts +1 -0
  29. package/dist/src/room/utils.d.ts.map +1 -1
  30. package/dist/ts4.2/src/connectionHelper/ConnectionCheck.d.ts +2 -0
  31. package/dist/ts4.2/src/connectionHelper/checks/Checker.d.ts +5 -2
  32. package/dist/ts4.2/src/connectionHelper/checks/cloudRegion.d.ts +18 -0
  33. package/dist/ts4.2/src/connectionHelper/checks/connectionProtocol.d.ts +20 -0
  34. package/dist/ts4.2/src/connectionHelper/checks/publishVideo.d.ts +1 -0
  35. package/dist/ts4.2/src/index.d.ts +2 -2
  36. package/dist/ts4.2/src/room/StreamReader.d.ts +3 -3
  37. package/dist/ts4.2/src/room/StreamWriter.d.ts +3 -12
  38. package/dist/ts4.2/src/room/types.d.ts +0 -5
  39. package/dist/ts4.2/src/room/utils.d.ts +1 -0
  40. package/package.json +17 -17
  41. package/src/connectionHelper/ConnectionCheck.ts +15 -0
  42. package/src/connectionHelper/checks/Checker.ts +41 -8
  43. package/src/connectionHelper/checks/cloudRegion.ts +94 -0
  44. package/src/connectionHelper/checks/connectionProtocol.ts +149 -0
  45. package/src/connectionHelper/checks/publishAudio.ts +8 -0
  46. package/src/connectionHelper/checks/publishVideo.ts +52 -0
  47. package/src/index.ts +1 -1
  48. package/src/room/StreamReader.ts +8 -15
  49. package/src/room/StreamWriter.ts +4 -4
  50. package/src/room/participant/LocalParticipant.ts +9 -26
  51. package/src/room/participant/publishUtils.ts +4 -0
  52. package/src/room/track/LocalTrack.ts +5 -2
  53. package/src/room/track/create.ts +9 -5
  54. package/src/room/types.ts +0 -6
  55. package/src/room/utils.ts +16 -0
@@ -6666,6 +6666,18 @@ PERFORMANCE OF THIS SOFTWARE.
6666
6666
  /* global Reflect, Promise, SuppressedError, Symbol, Iterator */
6667
6667
 
6668
6668
 
6669
+ function __rest(s, e) {
6670
+ var t = {};
6671
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
6672
+ t[p] = s[p];
6673
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6674
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
6675
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
6676
+ t[p[i]] = s[p[i]];
6677
+ }
6678
+ return t;
6679
+ }
6680
+
6669
6681
  function __awaiter(thisArg, _arguments, P, generator) {
6670
6682
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
6671
6683
  return new (P || (P = Promise))(function (resolve, reject) {
@@ -11150,7 +11162,7 @@ function getOSVersion(ua) {
11150
11162
  return ua.includes('mac os') ? getMatch(/\(.+?(\d+_\d+(:?_\d+)?)/, ua, 1).replace(/_/g, '.') : undefined;
11151
11163
  }
11152
11164
 
11153
- var version$1 = "2.9.1";
11165
+ var version$1 = "2.9.2";
11154
11166
 
11155
11167
  const version = version$1;
11156
11168
  const protocolVersion = 15;
@@ -12130,6 +12142,21 @@ function isLocalParticipant(p) {
12130
12142
  function isRemoteParticipant(p) {
12131
12143
  return !p.isLocal;
12132
12144
  }
12145
+ function splitUtf8(s, n) {
12146
+ // adapted from https://stackoverflow.com/a/6043797
12147
+ const result = [];
12148
+ while (s.length > n) {
12149
+ let k = n;
12150
+ // Move back to find the start of a UTF-8 character
12151
+ while ((s.charCodeAt(k) & 0xc0) === 0x80) {
12152
+ k--;
12153
+ }
12154
+ result.push(s.slice(0, k));
12155
+ s = s.slice(k);
12156
+ }
12157
+ result.push(s);
12158
+ return result;
12159
+ }
12133
12160
 
12134
12161
  function mergeDefaultOptions(options, audioDefaults, videoDefaults) {
12135
12162
  var _a, _b, _c;
@@ -15557,6 +15584,11 @@ class LocalTrack extends Track {
15557
15584
  if (!constraints) {
15558
15585
  constraints = this._constraints;
15559
15586
  }
15587
+ const _a = this._constraints,
15588
+ {
15589
+ deviceId
15590
+ } = _a,
15591
+ otherConstraints = __rest(_a, ["deviceId"]);
15560
15592
  this.log.debug('restarting track with constraints', Object.assign(Object.assign({}, this.logContext), {
15561
15593
  constraints
15562
15594
  }));
@@ -15565,9 +15597,13 @@ class LocalTrack extends Track {
15565
15597
  video: false
15566
15598
  };
15567
15599
  if (this.kind === Track.Kind.Video) {
15568
- streamConstraints.video = constraints;
15600
+ streamConstraints.video = deviceId ? {
15601
+ deviceId
15602
+ } : true;
15569
15603
  } else {
15570
- streamConstraints.audio = constraints;
15604
+ streamConstraints.audio = deviceId ? {
15605
+ deviceId
15606
+ } : true;
15571
15607
  }
15572
15608
  // these steps are duplicated from setMediaStreamTrack because we must stop
15573
15609
  // the previous tracks before new tracks can be acquired
@@ -15582,6 +15618,7 @@ class LocalTrack extends Track {
15582
15618
  // create new track and attach
15583
15619
  const mediaStream = yield navigator.mediaDevices.getUserMedia(streamConstraints);
15584
15620
  const newTrack = mediaStream.getTracks()[0];
15621
+ yield newTrack.applyConstraints(otherConstraints);
15585
15622
  newTrack.addEventListener('ended', this.handleEnded);
15586
15623
  this.log.debug('re-acquired MediaStreamTrack', this.logContext);
15587
15624
  yield this.setMediaStreamTrack(newTrack);
@@ -16191,6 +16228,10 @@ function computeTrackBackupEncodings(track, videoCodec, opts) {
16191
16228
  const settings = track.mediaStreamTrack.getSettings();
16192
16229
  const width = (_a = settings.width) !== null && _a !== void 0 ? _a : (_b = track.dimensions) === null || _b === void 0 ? void 0 : _b.width;
16193
16230
  const height = (_c = settings.height) !== null && _c !== void 0 ? _c : (_d = track.dimensions) === null || _d === void 0 ? void 0 : _d.height;
16231
+ // disable simulcast for screenshare backup codec since L1Tx is used by primary codec
16232
+ if (track.source === Track.Source.ScreenShare && opts.simulcast) {
16233
+ opts.simulcast = false;
16234
+ }
16194
16235
  const encodings = computeVideoEncodings(track.source === Track.Source.ScreenShare, width, height, opts);
16195
16236
  return encodings;
16196
16237
  }
@@ -18324,11 +18365,7 @@ class TextStreamReader extends BaseStreamReader {
18324
18365
  this.handleChunkReceived(value);
18325
18366
  return {
18326
18367
  done: false,
18327
- value: {
18328
- index: bigIntToNumber(value.chunkIndex),
18329
- current: decoder.decode(value.content),
18330
- collected: Array.from(this.receivedChunks.values()).sort((a, b) => bigIntToNumber(a.chunkIndex) - bigIntToNumber(b.chunkIndex)).map(chunk => decoder.decode(chunk.content)).join('')
18331
- }
18368
+ value: decoder.decode(value.content)
18332
18369
  };
18333
18370
  }
18334
18371
  } catch (error) {
@@ -18353,15 +18390,13 @@ class TextStreamReader extends BaseStreamReader {
18353
18390
  readAll() {
18354
18391
  return __awaiter(this, void 0, void 0, function* () {
18355
18392
  var _a, e_2, _b, _c;
18356
- let latestString = '';
18393
+ let finalString = '';
18357
18394
  try {
18358
18395
  for (var _d = true, _e = __asyncValues(this), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
18359
18396
  _c = _f.value;
18360
18397
  _d = false;
18361
- const {
18362
- collected
18363
- } = _c;
18364
- latestString = collected;
18398
+ const chunk = _c;
18399
+ finalString += chunk;
18365
18400
  }
18366
18401
  } catch (e_2_1) {
18367
18402
  e_2 = {
@@ -18374,7 +18409,7 @@ class TextStreamReader extends BaseStreamReader {
18374
18409
  if (e_2) throw e_2.error;
18375
18410
  }
18376
18411
  }
18377
- return latestString;
18412
+ return finalString;
18378
18413
  });
18379
18414
  }
18380
18415
  }
@@ -18387,7 +18422,7 @@ class BaseStreamWriter {
18387
18422
  this.info = info;
18388
18423
  }
18389
18424
  write(chunk) {
18390
- return this.defaultWriter.write([chunk]);
18425
+ return this.defaultWriter.write(chunk);
18391
18426
  }
18392
18427
  close() {
18393
18428
  return __awaiter(this, void 0, void 0, function* () {
@@ -20794,14 +20829,9 @@ class LocalParticipant extends Participant {
20794
20829
  topic: options === null || options === void 0 ? void 0 : options.topic,
20795
20830
  attachedStreamIds: fileIds
20796
20831
  });
20797
- const textChunkSize = Math.floor(STREAM_CHUNK_SIZE / 4); // utf8 is at most 4 bytes long, so play it safe and take a quarter of the byte size to slice the string
20798
- const totalTextChunks = Math.ceil(totalTextLength / textChunkSize);
20799
- for (let i = 0; i < totalTextChunks; i++) {
20800
- const chunkData = text.slice(i * textChunkSize, Math.min((i + 1) * textChunkSize, totalTextLength));
20801
- yield this.engine.waitForBufferStatusLow(DataPacket_Kind.RELIABLE);
20802
- yield writer.write(chunkData);
20803
- handleProgress(Math.ceil((i + 1) / totalTextChunks), 0);
20804
- }
20832
+ yield writer.write(text);
20833
+ // set text part of progress to 1
20834
+ handleProgress(1, 0);
20805
20835
  yield writer.close();
20806
20836
  if ((options === null || options === void 0 ? void 0 : options.attachments) && fileIds) {
20807
20837
  yield Promise.all(options.attachments.map((file, idx) => __awaiter(this, void 0, void 0, function* () {
@@ -20861,32 +20891,26 @@ class LocalParticipant extends Participant {
20861
20891
  const localP = this;
20862
20892
  const writableStream = new WritableStream({
20863
20893
  // Implement the sink
20864
- write(_ref3) {
20865
- let [textChunk] = _ref3;
20866
- var _a;
20867
- const textInBytes = new TextEncoder().encode(textChunk);
20868
- if (textInBytes.byteLength > STREAM_CHUNK_SIZE) {
20869
- (_a = this.abort) === null || _a === void 0 ? void 0 : _a.call(this);
20870
- throw new Error('chunk size too large');
20871
- }
20872
- return new Promise(resolve => __awaiter(this, void 0, void 0, function* () {
20873
- yield localP.engine.waitForBufferStatusLow(DataPacket_Kind.RELIABLE);
20874
- const chunk = new DataStream_Chunk({
20875
- content: textInBytes,
20876
- streamId,
20877
- chunkIndex: numberToBigInt(chunkId)
20878
- });
20879
- const chunkPacket = new DataPacket({
20880
- destinationIdentities,
20881
- value: {
20882
- case: 'streamChunk',
20883
- value: chunk
20884
- }
20885
- });
20886
- yield localP.engine.sendDataPacket(chunkPacket, DataPacket_Kind.RELIABLE);
20887
- chunkId += 1;
20888
- resolve();
20889
- }));
20894
+ write(text) {
20895
+ return __awaiter(this, void 0, void 0, function* () {
20896
+ for (const textChunk in splitUtf8(text, STREAM_CHUNK_SIZE)) {
20897
+ yield localP.engine.waitForBufferStatusLow(DataPacket_Kind.RELIABLE);
20898
+ const chunk = new DataStream_Chunk({
20899
+ content: new TextEncoder().encode(textChunk),
20900
+ streamId,
20901
+ chunkIndex: numberToBigInt(chunkId)
20902
+ });
20903
+ const chunkPacket = new DataPacket({
20904
+ destinationIdentities,
20905
+ value: {
20906
+ case: 'streamChunk',
20907
+ value: chunk
20908
+ }
20909
+ });
20910
+ yield localP.engine.sendDataPacket(chunkPacket, DataPacket_Kind.RELIABLE);
20911
+ chunkId += 1;
20912
+ }
20913
+ });
20890
20914
  },
20891
20915
  close() {
20892
20916
  return __awaiter(this, void 0, void 0, function* () {
@@ -21000,14 +21024,14 @@ class LocalParticipant extends Participant {
21000
21024
  * @throws Error on failure. Details in `message`.
21001
21025
  */
21002
21026
  performRpc(_a) {
21003
- return __awaiter(this, arguments, void 0, function (_ref4) {
21027
+ return __awaiter(this, arguments, void 0, function (_ref3) {
21004
21028
  var _this5 = this;
21005
21029
  let {
21006
21030
  destinationIdentity,
21007
21031
  method,
21008
21032
  payload,
21009
21033
  responseTimeout = 10000
21010
- } = _ref4;
21034
+ } = _ref3;
21011
21035
  return function* () {
21012
21036
  const maxRoundTripLatency = 2000;
21013
21037
  return new Promise((resolve, reject) => __awaiter(_this5, void 0, void 0, function* () {
@@ -21212,8 +21236,8 @@ class LocalParticipant extends Participant {
21212
21236
  const waitForPendingTimeout = 10000;
21213
21237
  const startTime = Date.now();
21214
21238
  while (Date.now() < startTime + waitForPendingTimeout) {
21215
- const publishPromiseEntry = Array.from(this.pendingPublishPromises.entries()).find(_ref5 => {
21216
- let [pendingTrack] = _ref5;
21239
+ const publishPromiseEntry = Array.from(this.pendingPublishPromises.entries()).find(_ref4 => {
21240
+ let [pendingTrack] = _ref4;
21217
21241
  return pendingTrack.source === source;
21218
21242
  });
21219
21243
  if (publishPromiseEntry) {
@@ -23670,15 +23694,13 @@ class Checker extends eventsExports.EventEmitter {
23670
23694
  super();
23671
23695
  this.status = CheckStatus.IDLE;
23672
23696
  this.logs = [];
23673
- this.errorsAsWarnings = false;
23697
+ this.options = {};
23674
23698
  this.url = url;
23675
23699
  this.token = token;
23676
23700
  this.name = this.constructor.name;
23677
23701
  this.room = new Room(options.roomOptions);
23678
23702
  this.connectOptions = options.connectOptions;
23679
- if (options.errorsAsWarnings) {
23680
- this.errorsAsWarnings = options.errorsAsWarnings;
23681
- }
23703
+ this.options = options;
23682
23704
  }
23683
23705
  run(onComplete) {
23684
23706
  return __awaiter(this, void 0, void 0, function* () {
@@ -23690,7 +23712,7 @@ class Checker extends eventsExports.EventEmitter {
23690
23712
  yield this.perform();
23691
23713
  } catch (err) {
23692
23714
  if (err instanceof Error) {
23693
- if (this.errorsAsWarnings) {
23715
+ if (this.options.errorsAsWarnings) {
23694
23716
  this.appendWarning(err.message);
23695
23717
  } else {
23696
23718
  this.appendError(err.message);
@@ -23713,12 +23735,15 @@ class Checker extends eventsExports.EventEmitter {
23713
23735
  isSuccess() {
23714
23736
  return !this.logs.some(l => l.level === 'error');
23715
23737
  }
23716
- connect() {
23738
+ connect(url) {
23717
23739
  return __awaiter(this, void 0, void 0, function* () {
23718
23740
  if (this.room.state === ConnectionState.Connected) {
23719
23741
  return this.room;
23720
23742
  }
23721
- yield this.room.connect(this.url, this.token, this.connectOptions);
23743
+ if (!url) {
23744
+ url = this.url;
23745
+ }
23746
+ yield this.room.connect(url, this.token, this.connectOptions);
23722
23747
  return this.room;
23723
23748
  });
23724
23749
  }
@@ -23734,6 +23759,33 @@ class Checker extends eventsExports.EventEmitter {
23734
23759
  skip() {
23735
23760
  this.setStatus(CheckStatus.SKIPPED);
23736
23761
  }
23762
+ switchProtocol(protocol) {
23763
+ return __awaiter(this, void 0, void 0, function* () {
23764
+ let hasReconnecting = false;
23765
+ let hasReconnected = false;
23766
+ this.room.on(RoomEvent.Reconnecting, () => {
23767
+ hasReconnecting = true;
23768
+ });
23769
+ this.room.once(RoomEvent.Reconnected, () => {
23770
+ hasReconnected = true;
23771
+ });
23772
+ this.room.simulateScenario("force-".concat(protocol));
23773
+ yield new Promise(resolve => setTimeout(resolve, 1000));
23774
+ if (!hasReconnecting) {
23775
+ // no need to wait for reconnection
23776
+ return;
23777
+ }
23778
+ // wait for 10 seconds for reconnection
23779
+ const timeout = Date.now() + 10000;
23780
+ while (Date.now() < timeout) {
23781
+ if (hasReconnected) {
23782
+ return;
23783
+ }
23784
+ yield sleep(100);
23785
+ }
23786
+ throw new Error("Could not reconnect using ".concat(protocol, " protocol after 10 seconds"));
23787
+ });
23788
+ }
23737
23789
  appendMessage(message) {
23738
23790
  this.logs.push({
23739
23791
  level: 'info',
@@ -23773,6 +23825,201 @@ class Checker extends eventsExports.EventEmitter {
23773
23825
  }
23774
23826
  }
23775
23827
 
23828
+ /**
23829
+ * Checks for connections quality to closests Cloud regions and determining the best quality
23830
+ */
23831
+ class CloudRegionCheck extends Checker {
23832
+ get description() {
23833
+ return 'Cloud regions';
23834
+ }
23835
+ perform() {
23836
+ return __awaiter(this, void 0, void 0, function* () {
23837
+ const regionProvider = new RegionUrlProvider(this.url, this.token);
23838
+ if (!regionProvider.isCloud()) {
23839
+ this.skip();
23840
+ return;
23841
+ }
23842
+ const regionStats = [];
23843
+ const seenUrls = new Set();
23844
+ for (let i = 0; i < 3; i++) {
23845
+ const regionUrl = yield regionProvider.getNextBestRegionUrl();
23846
+ if (!regionUrl) {
23847
+ break;
23848
+ }
23849
+ if (seenUrls.has(regionUrl)) {
23850
+ continue;
23851
+ }
23852
+ seenUrls.add(regionUrl);
23853
+ const stats = yield this.checkCloudRegion(regionUrl);
23854
+ this.appendMessage("".concat(stats.region, " RTT: ").concat(stats.rtt, "ms, duration: ").concat(stats.duration, "ms"));
23855
+ regionStats.push(stats);
23856
+ }
23857
+ regionStats.sort((a, b) => {
23858
+ return (a.duration - b.duration) * 0.5 + (a.rtt - b.rtt) * 0.5;
23859
+ });
23860
+ const bestRegion = regionStats[0];
23861
+ this.bestStats = bestRegion;
23862
+ this.appendMessage("best Cloud region: ".concat(bestRegion.region));
23863
+ });
23864
+ }
23865
+ getInfo() {
23866
+ const info = super.getInfo();
23867
+ info.data = this.bestStats;
23868
+ return info;
23869
+ }
23870
+ checkCloudRegion(url) {
23871
+ return __awaiter(this, void 0, void 0, function* () {
23872
+ var _a, _b;
23873
+ yield this.connect(url);
23874
+ if (this.options.protocol === 'tcp') {
23875
+ yield this.switchProtocol('tcp');
23876
+ }
23877
+ const region = (_a = this.room.serverInfo) === null || _a === void 0 ? void 0 : _a.region;
23878
+ if (!region) {
23879
+ throw new Error('Region not found');
23880
+ }
23881
+ const writer = yield this.room.localParticipant.streamText({
23882
+ topic: 'test'
23883
+ });
23884
+ const chunkSize = 1000; // each chunk is about 1000 bytes
23885
+ const totalSize = 1000000; // approximately 1MB of data
23886
+ const numChunks = totalSize / chunkSize; // will yield 1000 chunks
23887
+ const chunkData = 'A'.repeat(chunkSize); // create a string of 1000 'A' characters
23888
+ const startTime = Date.now();
23889
+ for (let i = 0; i < numChunks; i++) {
23890
+ yield writer.write(chunkData);
23891
+ }
23892
+ yield writer.close();
23893
+ const endTime = Date.now();
23894
+ const stats = yield (_b = this.room.engine.pcManager) === null || _b === void 0 ? void 0 : _b.publisher.getStats();
23895
+ const regionStats = {
23896
+ region: region,
23897
+ rtt: 10000,
23898
+ duration: endTime - startTime
23899
+ };
23900
+ stats === null || stats === void 0 ? void 0 : stats.forEach(stat => {
23901
+ if (stat.type === 'candidate-pair' && stat.nominated) {
23902
+ regionStats.rtt = stat.currentRoundTripTime * 1000;
23903
+ }
23904
+ });
23905
+ yield this.disconnect();
23906
+ return regionStats;
23907
+ });
23908
+ }
23909
+ }
23910
+
23911
+ const TEST_DURATION = 10000;
23912
+ class ConnectionProtocolCheck extends Checker {
23913
+ get description() {
23914
+ return 'Connection via UDP vs TCP';
23915
+ }
23916
+ perform() {
23917
+ return __awaiter(this, void 0, void 0, function* () {
23918
+ const udpStats = yield this.checkConnectionProtocol('udp');
23919
+ const tcpStats = yield this.checkConnectionProtocol('tcp');
23920
+ this.bestStats = udpStats;
23921
+ // udp should is the better protocol typically. however, we'd prefer TCP when either of these conditions are true:
23922
+ // 1. the bandwidth limitation is worse on UDP by 500ms
23923
+ // 2. the packet loss is higher on UDP by 1%
23924
+ if (udpStats.qualityLimitationDurations.bandwidth - tcpStats.qualityLimitationDurations.bandwidth > 0.5 || (udpStats.packetsLost - tcpStats.packetsLost) / udpStats.packetsSent > 0.01) {
23925
+ this.appendMessage('best connection quality via tcp');
23926
+ this.bestStats = tcpStats;
23927
+ } else {
23928
+ this.appendMessage('best connection quality via udp');
23929
+ }
23930
+ const stats = this.bestStats;
23931
+ this.appendMessage("upstream bitrate: ".concat((stats.bitrateTotal / stats.count / 1000 / 1000).toFixed(2), " mbps"));
23932
+ this.appendMessage("RTT: ".concat((stats.rttTotal / stats.count * 1000).toFixed(2), " ms"));
23933
+ this.appendMessage("jitter: ".concat((stats.jitterTotal / stats.count * 1000).toFixed(2), " ms"));
23934
+ if (stats.packetsLost > 0) {
23935
+ this.appendWarning("packets lost: ".concat((stats.packetsLost / stats.packetsSent * 100).toFixed(2), "%"));
23936
+ }
23937
+ if (stats.qualityLimitationDurations.bandwidth > 1) {
23938
+ this.appendWarning("bandwidth limited ".concat((stats.qualityLimitationDurations.bandwidth / (TEST_DURATION / 1000) * 100).toFixed(2), "%"));
23939
+ }
23940
+ if (stats.qualityLimitationDurations.cpu > 0) {
23941
+ this.appendWarning("cpu limited ".concat((stats.qualityLimitationDurations.cpu / (TEST_DURATION / 1000) * 100).toFixed(2), "%"));
23942
+ }
23943
+ });
23944
+ }
23945
+ getInfo() {
23946
+ const info = super.getInfo();
23947
+ info.data = this.bestStats;
23948
+ return info;
23949
+ }
23950
+ checkConnectionProtocol(protocol) {
23951
+ return __awaiter(this, void 0, void 0, function* () {
23952
+ yield this.connect();
23953
+ if (protocol === 'tcp') {
23954
+ yield this.switchProtocol('tcp');
23955
+ } else {
23956
+ yield this.switchProtocol('udp');
23957
+ }
23958
+ // create a canvas with animated content
23959
+ const canvas = document.createElement('canvas');
23960
+ canvas.width = 1280;
23961
+ canvas.height = 720;
23962
+ const ctx = canvas.getContext('2d');
23963
+ if (!ctx) {
23964
+ throw new Error('Could not get canvas context');
23965
+ }
23966
+ let hue = 0;
23967
+ const animate = () => {
23968
+ hue = (hue + 1) % 360;
23969
+ ctx.fillStyle = "hsl(".concat(hue, ", 100%, 50%)");
23970
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
23971
+ requestAnimationFrame(animate);
23972
+ };
23973
+ animate();
23974
+ // create video track from canvas
23975
+ const stream = canvas.captureStream(30); // 30fps
23976
+ const videoTrack = stream.getVideoTracks()[0];
23977
+ // publish to room
23978
+ const pub = yield this.room.localParticipant.publishTrack(videoTrack, {
23979
+ simulcast: false,
23980
+ degradationPreference: 'maintain-resolution',
23981
+ videoEncoding: {
23982
+ maxBitrate: 2000000
23983
+ }
23984
+ });
23985
+ const track = pub.track;
23986
+ const protocolStats = {
23987
+ protocol,
23988
+ packetsLost: 0,
23989
+ packetsSent: 0,
23990
+ qualityLimitationDurations: {},
23991
+ rttTotal: 0,
23992
+ jitterTotal: 0,
23993
+ bitrateTotal: 0,
23994
+ count: 0
23995
+ };
23996
+ // gather stats once a second
23997
+ const interval = setInterval(() => __awaiter(this, void 0, void 0, function* () {
23998
+ const stats = yield track.getRTCStatsReport();
23999
+ stats === null || stats === void 0 ? void 0 : stats.forEach(stat => {
24000
+ if (stat.type === 'outbound-rtp') {
24001
+ protocolStats.packetsSent = stat.packetsSent;
24002
+ protocolStats.qualityLimitationDurations = stat.qualityLimitationDurations;
24003
+ protocolStats.bitrateTotal += stat.targetBitrate;
24004
+ protocolStats.count++;
24005
+ } else if (stat.type === 'remote-inbound-rtp') {
24006
+ protocolStats.packetsLost = stat.packetsLost;
24007
+ protocolStats.rttTotal += stat.roundTripTime;
24008
+ protocolStats.jitterTotal += stat.jitter;
24009
+ }
24010
+ });
24011
+ }), 1000);
24012
+ // wait a bit to gather stats
24013
+ yield new Promise(resolve => setTimeout(resolve, TEST_DURATION));
24014
+ clearInterval(interval);
24015
+ videoTrack.stop();
24016
+ canvas.remove();
24017
+ yield this.disconnect();
24018
+ return protocolStats;
24019
+ });
24020
+ }
24021
+ }
24022
+
23776
24023
  /**
23777
24024
  * Creates a local video and audio track at the same time. When acquiring both
23778
24025
  * audio and video tracks together, it'll display a single permission prompt to
@@ -23818,11 +24065,12 @@ function createLocalTracks(options) {
23818
24065
  }
23819
24066
  // update the constraints with the device id the user gave permissions to in the permission prompt
23820
24067
  // otherwise each track restart (e.g. mute - unmute) will try to initialize the device again -> causing additional permission prompts
23821
- if (trackConstraints) {
23822
- trackConstraints.deviceId = mediaStreamTrack.getSettings().deviceId;
23823
- } else {
24068
+ const newDeviceId = mediaStreamTrack.getSettings().deviceId;
24069
+ if ((trackConstraints === null || trackConstraints === void 0 ? void 0 : trackConstraints.deviceId) && unwrapConstraint(trackConstraints.deviceId) !== newDeviceId) {
24070
+ trackConstraints.deviceId = newDeviceId;
24071
+ } else if (!trackConstraints) {
23824
24072
  trackConstraints = {
23825
- deviceId: mediaStreamTrack.getSettings().deviceId
24073
+ deviceId: newDeviceId
23826
24074
  };
23827
24075
  }
23828
24076
  const track = mediaTrackToLocalTrack(mediaStreamTrack, trackConstraints);
@@ -23906,6 +24154,11 @@ class PublishAudioCheck extends Checker {
23906
24154
  var _a;
23907
24155
  const room = yield this.connect();
23908
24156
  const track = yield createLocalAudioTrack();
24157
+ const trackIsSilent = yield detectSilence(track, 1000);
24158
+ if (trackIsSilent) {
24159
+ throw new Error('unable to detect audio from microphone');
24160
+ }
24161
+ this.appendMessage('detected audio from microphone');
23909
24162
  room.localParticipant.publishTrack(track);
23910
24163
  // wait for a few seconds to publish
23911
24164
  yield new Promise(resolve => setTimeout(resolve, 3000));
@@ -23937,6 +24190,8 @@ class PublishVideoCheck extends Checker {
23937
24190
  var _a;
23938
24191
  const room = yield this.connect();
23939
24192
  const track = yield createLocalVideoTrack();
24193
+ // check if we have video from camera
24194
+ yield this.checkForVideo(track.mediaStreamTrack);
23940
24195
  room.localParticipant.publishTrack(track);
23941
24196
  // wait for a few seconds to publish
23942
24197
  yield new Promise(resolve => setTimeout(resolve, 5000));
@@ -23957,6 +24212,50 @@ class PublishVideoCheck extends Checker {
23957
24212
  this.appendMessage("published ".concat(numPackets, " video packets"));
23958
24213
  });
23959
24214
  }
24215
+ checkForVideo(track) {
24216
+ return __awaiter(this, void 0, void 0, function* () {
24217
+ const stream = new MediaStream();
24218
+ stream.addTrack(track.clone());
24219
+ // Create video element to check frames
24220
+ const video = document.createElement('video');
24221
+ video.srcObject = stream;
24222
+ video.muted = true;
24223
+ yield new Promise(resolve => {
24224
+ video.onplay = () => {
24225
+ setTimeout(() => {
24226
+ var _a, _b, _c, _d;
24227
+ const canvas = document.createElement('canvas');
24228
+ const settings = track.getSettings();
24229
+ const width = (_b = (_a = settings.width) !== null && _a !== void 0 ? _a : video.videoWidth) !== null && _b !== void 0 ? _b : 1280;
24230
+ const height = (_d = (_c = settings.height) !== null && _c !== void 0 ? _c : video.videoHeight) !== null && _d !== void 0 ? _d : 720;
24231
+ canvas.width = width;
24232
+ canvas.height = height;
24233
+ const ctx = canvas.getContext('2d');
24234
+ // Draw video frame to canvas
24235
+ ctx.drawImage(video, 0, 0);
24236
+ // Get image data and check if all pixels are black
24237
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
24238
+ const data = imageData.data;
24239
+ let isAllBlack = true;
24240
+ for (let i = 0; i < data.length; i += 4) {
24241
+ if (data[i] !== 0 || data[i + 1] !== 0 || data[i + 2] !== 0) {
24242
+ isAllBlack = false;
24243
+ break;
24244
+ }
24245
+ }
24246
+ if (isAllBlack) {
24247
+ this.appendError('camera appears to be producing only black frames');
24248
+ } else {
24249
+ this.appendMessage('received video frames');
24250
+ }
24251
+ resolve();
24252
+ }, 1000);
24253
+ };
24254
+ video.play();
24255
+ });
24256
+ video.remove();
24257
+ });
24258
+ }
23960
24259
  }
23961
24260
 
23962
24261
  class ReconnectCheck extends Checker {
@@ -24222,6 +24521,21 @@ class ConnectionCheck extends eventsExports.EventEmitter {
24222
24521
  return this.createAndRunCheck(PublishVideoCheck);
24223
24522
  });
24224
24523
  }
24524
+ checkConnectionProtocol() {
24525
+ return __awaiter(this, void 0, void 0, function* () {
24526
+ const info = yield this.createAndRunCheck(ConnectionProtocolCheck);
24527
+ if (info.data && 'protocol' in info.data) {
24528
+ const stats = info.data;
24529
+ this.options.protocol = stats.protocol;
24530
+ }
24531
+ return info;
24532
+ });
24533
+ }
24534
+ checkCloudRegion() {
24535
+ return __awaiter(this, void 0, void 0, function* () {
24536
+ return this.createAndRunCheck(CloudRegionCheck);
24537
+ });
24538
+ }
24225
24539
  }
24226
24540
 
24227
24541
  /**
@@ -24305,5 +24619,5 @@ function isFacingModeValue(item) {
24305
24619
  return item === undefined || allowedValues.includes(item);
24306
24620
  }
24307
24621
 
24308
- export { AudioPresets, BackupCodecPolicy, BaseKeyProvider, CheckStatus, Checker, ConnectionCheck, ConnectionError, ConnectionErrorReason, ConnectionQuality, ConnectionState, CriticalTimers, CryptorError, CryptorErrorReason, CryptorEvent, DataPacket_Kind, DefaultReconnectPolicy, DeviceUnsupportedError, DisconnectReason, EncryptionEvent, EngineEvent, ExternalE2EEKeyProvider, KeyHandlerEvent, KeyProviderEvent, LivekitError, LocalAudioTrack, LocalParticipant, LocalTrack, LocalTrackPublication, LocalVideoTrack, LogLevel, LoggerNames, MediaDeviceFailure, _ as Mutex, NegotiationError, Participant, ParticipantEvent, ParticipantInfo_Kind as ParticipantKind, PublishDataError, RemoteAudioTrack, RemoteParticipant, RemoteTrack, RemoteTrackPublication, RemoteVideoTrack, Room, RoomEvent, RpcError, ScreenSharePresets, SignalRequestError, SubscriptionError, Track, TrackEvent, TrackInvalidError, TrackPublication, UnexpectedConnectionState, UnsupportedServer, VideoPreset, VideoPresets, VideoPresets43, VideoQuality, attachToElement, compareVersions, createAudioAnalyser, createE2EEKey, createKeyMaterialFromBuffer, createKeyMaterialFromString, createLocalAudioTrack, createLocalScreenTracks, createLocalTracks, createLocalVideoTrack, deriveKeys, detachTrack, facingModeFromDeviceLabel, facingModeFromLocalTrack, getBrowser, getEmptyAudioStreamTrack, getEmptyVideoStreamTrack, getLogger, importKey, isAudioTrack, isBackupCodec, isBrowserSupported, isE2EESupported, isInsertableStreamSupported, isLocalParticipant, isLocalTrack, isRemoteParticipant, isRemoteTrack, isScriptTransformSupported, isVideoFrame, isVideoTrack, needsRbspUnescaping, parseRbsp, protocolVersion, ratchet, setLogExtension, setLogLevel, supportsAV1, supportsAdaptiveStream, supportsDynacast, supportsVP9, version, videoCodecs, writeRbsp };
24622
+ export { AudioPresets, BackupCodecPolicy, BaseKeyProvider, CheckStatus, Checker, ConnectionCheck, ConnectionError, ConnectionErrorReason, ConnectionQuality, ConnectionState, CriticalTimers, CryptorError, CryptorErrorReason, CryptorEvent, DataPacket_Kind, DefaultReconnectPolicy, DeviceUnsupportedError, DisconnectReason, EncryptionEvent, EngineEvent, ExternalE2EEKeyProvider, KeyHandlerEvent, KeyProviderEvent, LivekitError, LocalAudioTrack, LocalParticipant, LocalTrack, LocalTrackPublication, LocalVideoTrack, LogLevel, LoggerNames, MediaDeviceFailure, _ as Mutex, NegotiationError, Participant, ParticipantEvent, ParticipantInfo_Kind as ParticipantKind, PublishDataError, RemoteAudioTrack, RemoteParticipant, RemoteTrack, RemoteTrackPublication, RemoteVideoTrack, Room, RoomEvent, RpcError, ScreenSharePresets, SignalRequestError, SubscriptionError, Track, TrackEvent, TrackInvalidError, TrackPublication, TrackType, UnexpectedConnectionState, UnsupportedServer, VideoPreset, VideoPresets, VideoPresets43, VideoQuality, attachToElement, compareVersions, createAudioAnalyser, createE2EEKey, createKeyMaterialFromBuffer, createKeyMaterialFromString, createLocalAudioTrack, createLocalScreenTracks, createLocalTracks, createLocalVideoTrack, deriveKeys, detachTrack, facingModeFromDeviceLabel, facingModeFromLocalTrack, getBrowser, getEmptyAudioStreamTrack, getEmptyVideoStreamTrack, getLogger, importKey, isAudioTrack, isBackupCodec, isBrowserSupported, isE2EESupported, isInsertableStreamSupported, isLocalParticipant, isLocalTrack, isRemoteParticipant, isRemoteTrack, isScriptTransformSupported, isVideoFrame, isVideoTrack, needsRbspUnescaping, parseRbsp, protocolVersion, ratchet, setLogExtension, setLogLevel, supportsAV1, supportsAdaptiveStream, supportsDynacast, supportsVP9, version, videoCodecs, writeRbsp };
24309
24623
  //# sourceMappingURL=livekit-client.esm.mjs.map