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
@@ -2,6 +2,8 @@ import { EventEmitter } from 'events';
2
2
  import type TypedEmitter from 'typed-emitter';
3
3
  import type { CheckInfo, CheckerOptions, InstantiableCheck } from './checks/Checker';
4
4
  import { CheckStatus, Checker } from './checks/Checker';
5
+ import { CloudRegionCheck } from './checks/cloudRegion';
6
+ import { ConnectionProtocolCheck, type ProtocolStats } from './checks/connectionProtocol';
5
7
  import { PublishAudioCheck } from './checks/publishAudio';
6
8
  import { PublishVideoCheck } from './checks/publishVideo';
7
9
  import { ReconnectCheck } from './checks/reconnect';
@@ -86,6 +88,19 @@ export class ConnectionCheck extends (EventEmitter as new () => TypedEmitter<Con
86
88
  async checkPublishVideo() {
87
89
  return this.createAndRunCheck(PublishVideoCheck);
88
90
  }
91
+
92
+ async checkConnectionProtocol() {
93
+ const info = await this.createAndRunCheck(ConnectionProtocolCheck);
94
+ if (info.data && 'protocol' in info.data) {
95
+ const stats = info.data as ProtocolStats;
96
+ this.options.protocol = stats.protocol;
97
+ }
98
+ return info;
99
+ }
100
+
101
+ async checkCloudRegion() {
102
+ return this.createAndRunCheck(CloudRegionCheck);
103
+ }
89
104
  }
90
105
 
91
106
  type ConnectionCheckCallbacks = {
@@ -3,6 +3,9 @@ import type TypedEmitter from 'typed-emitter';
3
3
  import type { RoomConnectOptions, RoomOptions } from '../../options';
4
4
  import type RTCEngine from '../../room/RTCEngine';
5
5
  import Room, { ConnectionState } from '../../room/Room';
6
+ import { RoomEvent } from '../../room/events';
7
+ import type { SimulationScenario } from '../../room/types';
8
+ import { sleep } from '../../room/utils';
6
9
 
7
10
  type LogMessage = {
8
11
  level: 'info' | 'warning' | 'error';
@@ -22,12 +25,14 @@ export type CheckInfo = {
22
25
  logs: Array<LogMessage>;
23
26
  status: CheckStatus;
24
27
  description: string;
28
+ data?: any;
25
29
  };
26
30
 
27
31
  export interface CheckerOptions {
28
32
  errorsAsWarnings?: boolean;
29
33
  roomOptions?: RoomOptions;
30
34
  connectOptions?: RoomConnectOptions;
35
+ protocol?: 'udp' | 'tcp';
31
36
  }
32
37
 
33
38
  export abstract class Checker extends (EventEmitter as new () => TypedEmitter<CheckerCallbacks>) {
@@ -43,10 +48,10 @@ export abstract class Checker extends (EventEmitter as new () => TypedEmitter<Ch
43
48
 
44
49
  logs: Array<LogMessage> = [];
45
50
 
46
- errorsAsWarnings: boolean = false;
47
-
48
51
  name: string;
49
52
 
53
+ options: CheckerOptions = {};
54
+
50
55
  constructor(url: string, token: string, options: CheckerOptions = {}) {
51
56
  super();
52
57
  this.url = url;
@@ -54,9 +59,7 @@ export abstract class Checker extends (EventEmitter as new () => TypedEmitter<Ch
54
59
  this.name = this.constructor.name;
55
60
  this.room = new Room(options.roomOptions);
56
61
  this.connectOptions = options.connectOptions;
57
- if (options.errorsAsWarnings) {
58
- this.errorsAsWarnings = options.errorsAsWarnings;
59
- }
62
+ this.options = options;
60
63
  }
61
64
 
62
65
  abstract get description(): string;
@@ -73,7 +76,7 @@ export abstract class Checker extends (EventEmitter as new () => TypedEmitter<Ch
73
76
  await this.perform();
74
77
  } catch (err) {
75
78
  if (err instanceof Error) {
76
- if (this.errorsAsWarnings) {
79
+ if (this.options.errorsAsWarnings) {
77
80
  this.appendWarning(err.message);
78
81
  } else {
79
82
  this.appendError(err.message);
@@ -101,11 +104,14 @@ export abstract class Checker extends (EventEmitter as new () => TypedEmitter<Ch
101
104
  return !this.logs.some((l) => l.level === 'error');
102
105
  }
103
106
 
104
- protected async connect(): Promise<Room> {
107
+ protected async connect(url?: string): Promise<Room> {
105
108
  if (this.room.state === ConnectionState.Connected) {
106
109
  return this.room;
107
110
  }
108
- await this.room.connect(this.url, this.token, this.connectOptions);
111
+ if (!url) {
112
+ url = this.url;
113
+ }
114
+ await this.room.connect(url, this.token, this.connectOptions);
109
115
  return this.room;
110
116
  }
111
117
 
@@ -121,6 +127,33 @@ export abstract class Checker extends (EventEmitter as new () => TypedEmitter<Ch
121
127
  this.setStatus(CheckStatus.SKIPPED);
122
128
  }
123
129
 
130
+ protected async switchProtocol(protocol: 'udp' | 'tcp' | 'tls') {
131
+ let hasReconnecting = false;
132
+ let hasReconnected = false;
133
+ this.room.on(RoomEvent.Reconnecting, () => {
134
+ hasReconnecting = true;
135
+ });
136
+ this.room.once(RoomEvent.Reconnected, () => {
137
+ hasReconnected = true;
138
+ });
139
+ this.room.simulateScenario(`force-${protocol}` as SimulationScenario);
140
+ await new Promise((resolve) => setTimeout(resolve, 1000));
141
+ if (!hasReconnecting) {
142
+ // no need to wait for reconnection
143
+ return;
144
+ }
145
+
146
+ // wait for 10 seconds for reconnection
147
+ const timeout = Date.now() + 10000;
148
+ while (Date.now() < timeout) {
149
+ if (hasReconnected) {
150
+ return;
151
+ }
152
+ await sleep(100);
153
+ }
154
+ throw new Error(`Could not reconnect using ${protocol} protocol after 10 seconds`);
155
+ }
156
+
124
157
  protected appendMessage(message: string) {
125
158
  this.logs.push({ level: 'info', message });
126
159
  this.emit('update', this.getInfo());
@@ -0,0 +1,94 @@
1
+ import { RegionUrlProvider } from '../../room/RegionUrlProvider';
2
+ import { type CheckInfo, Checker } from './Checker';
3
+
4
+ export interface RegionStats {
5
+ region: string;
6
+ rtt: number;
7
+ duration: number;
8
+ }
9
+
10
+ /**
11
+ * Checks for connections quality to closests Cloud regions and determining the best quality
12
+ */
13
+ export class CloudRegionCheck extends Checker {
14
+ private bestStats?: RegionStats;
15
+
16
+ get description(): string {
17
+ return 'Cloud regions';
18
+ }
19
+
20
+ async perform(): Promise<void> {
21
+ const regionProvider = new RegionUrlProvider(this.url, this.token);
22
+ if (!regionProvider.isCloud()) {
23
+ this.skip();
24
+ return;
25
+ }
26
+
27
+ const regionStats: RegionStats[] = [];
28
+ const seenUrls: Set<string> = new Set();
29
+ for (let i = 0; i < 3; i++) {
30
+ const regionUrl = await regionProvider.getNextBestRegionUrl();
31
+ if (!regionUrl) {
32
+ break;
33
+ }
34
+ if (seenUrls.has(regionUrl)) {
35
+ continue;
36
+ }
37
+ seenUrls.add(regionUrl);
38
+ const stats = await this.checkCloudRegion(regionUrl);
39
+ this.appendMessage(`${stats.region} RTT: ${stats.rtt}ms, duration: ${stats.duration}ms`);
40
+ regionStats.push(stats);
41
+ }
42
+
43
+ regionStats.sort((a, b) => {
44
+ return (a.duration - b.duration) * 0.5 + (a.rtt - b.rtt) * 0.5;
45
+ });
46
+ const bestRegion = regionStats[0];
47
+ this.bestStats = bestRegion;
48
+ this.appendMessage(`best Cloud region: ${bestRegion.region}`);
49
+ }
50
+
51
+ getInfo(): CheckInfo {
52
+ const info = super.getInfo();
53
+ info.data = this.bestStats;
54
+ return info;
55
+ }
56
+
57
+ private async checkCloudRegion(url: string): Promise<RegionStats> {
58
+ await this.connect(url);
59
+ if (this.options.protocol === 'tcp') {
60
+ await this.switchProtocol('tcp');
61
+ }
62
+ const region = this.room.serverInfo?.region;
63
+ if (!region) {
64
+ throw new Error('Region not found');
65
+ }
66
+
67
+ const writer = await this.room.localParticipant.streamText({ topic: 'test' });
68
+ const chunkSize = 1000; // each chunk is about 1000 bytes
69
+ const totalSize = 1_000_000; // approximately 1MB of data
70
+ const numChunks = totalSize / chunkSize; // will yield 1000 chunks
71
+ const chunkData = 'A'.repeat(chunkSize); // create a string of 1000 'A' characters
72
+
73
+ const startTime = Date.now();
74
+ for (let i = 0; i < numChunks; i++) {
75
+ await writer.write(chunkData);
76
+ }
77
+ await writer.close();
78
+ const endTime = Date.now();
79
+ const stats = await this.room.engine.pcManager?.publisher.getStats();
80
+ const regionStats: RegionStats = {
81
+ region: region,
82
+ rtt: 10000,
83
+ duration: endTime - startTime,
84
+ };
85
+ stats?.forEach((stat) => {
86
+ if (stat.type === 'candidate-pair' && stat.nominated) {
87
+ regionStats.rtt = stat.currentRoundTripTime * 1000;
88
+ }
89
+ });
90
+
91
+ await this.disconnect();
92
+ return regionStats;
93
+ }
94
+ }
@@ -0,0 +1,149 @@
1
+ import { type CheckInfo, Checker } from './Checker';
2
+
3
+ export interface ProtocolStats {
4
+ protocol: 'udp' | 'tcp';
5
+ packetsLost: number;
6
+ packetsSent: number;
7
+ qualityLimitationDurations: Record<string, number>;
8
+ // total metrics measure sum of all measurements, along with a count
9
+ rttTotal: number;
10
+ jitterTotal: number;
11
+ bitrateTotal: number;
12
+ count: number;
13
+ }
14
+
15
+ const TEST_DURATION = 10000;
16
+
17
+ export class ConnectionProtocolCheck extends Checker {
18
+ private bestStats?: ProtocolStats;
19
+
20
+ get description(): string {
21
+ return 'Connection via UDP vs TCP';
22
+ }
23
+
24
+ async perform(): Promise<void> {
25
+ const udpStats = await this.checkConnectionProtocol('udp');
26
+ const tcpStats = await this.checkConnectionProtocol('tcp');
27
+ this.bestStats = udpStats;
28
+ // udp should is the better protocol typically. however, we'd prefer TCP when either of these conditions are true:
29
+ // 1. the bandwidth limitation is worse on UDP by 500ms
30
+ // 2. the packet loss is higher on UDP by 1%
31
+ if (
32
+ udpStats.qualityLimitationDurations.bandwidth -
33
+ tcpStats.qualityLimitationDurations.bandwidth >
34
+ 0.5 ||
35
+ (udpStats.packetsLost - tcpStats.packetsLost) / udpStats.packetsSent > 0.01
36
+ ) {
37
+ this.appendMessage('best connection quality via tcp');
38
+ this.bestStats = tcpStats;
39
+ } else {
40
+ this.appendMessage('best connection quality via udp');
41
+ }
42
+
43
+ const stats = this.bestStats;
44
+ this.appendMessage(
45
+ `upstream bitrate: ${(stats.bitrateTotal / stats.count / 1000 / 1000).toFixed(2)} mbps`,
46
+ );
47
+ this.appendMessage(`RTT: ${((stats.rttTotal / stats.count) * 1000).toFixed(2)} ms`);
48
+ this.appendMessage(`jitter: ${((stats.jitterTotal / stats.count) * 1000).toFixed(2)} ms`);
49
+
50
+ if (stats.packetsLost > 0) {
51
+ this.appendWarning(
52
+ `packets lost: ${((stats.packetsLost / stats.packetsSent) * 100).toFixed(2)}%`,
53
+ );
54
+ }
55
+ if (stats.qualityLimitationDurations.bandwidth > 1) {
56
+ this.appendWarning(
57
+ `bandwidth limited ${((stats.qualityLimitationDurations.bandwidth / (TEST_DURATION / 1000)) * 100).toFixed(2)}%`,
58
+ );
59
+ }
60
+ if (stats.qualityLimitationDurations.cpu > 0) {
61
+ this.appendWarning(
62
+ `cpu limited ${((stats.qualityLimitationDurations.cpu / (TEST_DURATION / 1000)) * 100).toFixed(2)}%`,
63
+ );
64
+ }
65
+ }
66
+
67
+ getInfo(): CheckInfo {
68
+ const info = super.getInfo();
69
+ info.data = this.bestStats;
70
+ return info;
71
+ }
72
+
73
+ private async checkConnectionProtocol(protocol: 'tcp' | 'udp'): Promise<ProtocolStats> {
74
+ await this.connect();
75
+ if (protocol === 'tcp') {
76
+ await this.switchProtocol('tcp');
77
+ } else {
78
+ await this.switchProtocol('udp');
79
+ }
80
+
81
+ // create a canvas with animated content
82
+ const canvas = document.createElement('canvas');
83
+ canvas.width = 1280;
84
+ canvas.height = 720;
85
+ const ctx = canvas.getContext('2d');
86
+ if (!ctx) {
87
+ throw new Error('Could not get canvas context');
88
+ }
89
+
90
+ let hue = 0;
91
+ const animate = () => {
92
+ hue = (hue + 1) % 360;
93
+ ctx.fillStyle = `hsl(${hue}, 100%, 50%)`;
94
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
95
+ requestAnimationFrame(animate);
96
+ };
97
+ animate();
98
+
99
+ // create video track from canvas
100
+ const stream = canvas.captureStream(30); // 30fps
101
+ const videoTrack = stream.getVideoTracks()[0];
102
+
103
+ // publish to room
104
+ const pub = await this.room.localParticipant.publishTrack(videoTrack, {
105
+ simulcast: false,
106
+ degradationPreference: 'maintain-resolution',
107
+ videoEncoding: {
108
+ maxBitrate: 2000000,
109
+ },
110
+ });
111
+ const track = pub!.track!;
112
+
113
+ const protocolStats: ProtocolStats = {
114
+ protocol,
115
+ packetsLost: 0,
116
+ packetsSent: 0,
117
+ qualityLimitationDurations: {},
118
+ rttTotal: 0,
119
+ jitterTotal: 0,
120
+ bitrateTotal: 0,
121
+ count: 0,
122
+ };
123
+ // gather stats once a second
124
+ const interval = setInterval(async () => {
125
+ const stats = await track.getRTCStatsReport();
126
+ stats?.forEach((stat) => {
127
+ if (stat.type === 'outbound-rtp') {
128
+ protocolStats.packetsSent = stat.packetsSent;
129
+ protocolStats.qualityLimitationDurations = stat.qualityLimitationDurations;
130
+ protocolStats.bitrateTotal += stat.targetBitrate;
131
+ protocolStats.count++;
132
+ } else if (stat.type === 'remote-inbound-rtp') {
133
+ protocolStats.packetsLost = stat.packetsLost;
134
+ protocolStats.rttTotal += stat.roundTripTime;
135
+ protocolStats.jitterTotal += stat.jitter;
136
+ }
137
+ });
138
+ }, 1000);
139
+
140
+ // wait a bit to gather stats
141
+ await new Promise((resolve) => setTimeout(resolve, TEST_DURATION));
142
+ clearInterval(interval);
143
+
144
+ videoTrack.stop();
145
+ canvas.remove();
146
+ await this.disconnect();
147
+ return protocolStats;
148
+ }
149
+ }
@@ -1,4 +1,5 @@
1
1
  import { createLocalAudioTrack } from '../../room/track/create';
2
+ import { detectSilence } from '../../room/track/utils';
2
3
  import { Checker } from './Checker';
3
4
 
4
5
  export class PublishAudioCheck extends Checker {
@@ -10,6 +11,13 @@ export class PublishAudioCheck extends Checker {
10
11
  const room = await this.connect();
11
12
 
12
13
  const track = await createLocalAudioTrack();
14
+
15
+ const trackIsSilent = await detectSilence(track, 1000);
16
+ if (trackIsSilent) {
17
+ throw new Error('unable to detect audio from microphone');
18
+ }
19
+ this.appendMessage('detected audio from microphone');
20
+
13
21
  room.localParticipant.publishTrack(track);
14
22
  // wait for a few seconds to publish
15
23
  await new Promise((resolve) => setTimeout(resolve, 3000));
@@ -10,6 +10,10 @@ export class PublishVideoCheck extends Checker {
10
10
  const room = await this.connect();
11
11
 
12
12
  const track = await createLocalVideoTrack();
13
+
14
+ // check if we have video from camera
15
+ await this.checkForVideo(track.mediaStreamTrack);
16
+
13
17
  room.localParticipant.publishTrack(track);
14
18
  // wait for a few seconds to publish
15
19
  await new Promise((resolve) => setTimeout(resolve, 5000));
@@ -33,4 +37,52 @@ export class PublishVideoCheck extends Checker {
33
37
  }
34
38
  this.appendMessage(`published ${numPackets} video packets`);
35
39
  }
40
+
41
+ async checkForVideo(track: MediaStreamTrack) {
42
+ const stream = new MediaStream();
43
+ stream.addTrack(track.clone());
44
+
45
+ // Create video element to check frames
46
+ const video = document.createElement('video');
47
+ video.srcObject = stream;
48
+ video.muted = true;
49
+
50
+ await new Promise<void>((resolve) => {
51
+ video.onplay = () => {
52
+ setTimeout(() => {
53
+ const canvas = document.createElement('canvas');
54
+ const settings = track.getSettings();
55
+ const width = settings.width ?? video.videoWidth ?? 1280;
56
+ const height = settings.height ?? video.videoHeight ?? 720;
57
+ canvas.width = width;
58
+ canvas.height = height;
59
+ const ctx = canvas.getContext('2d')!;
60
+
61
+ // Draw video frame to canvas
62
+ ctx.drawImage(video, 0, 0);
63
+
64
+ // Get image data and check if all pixels are black
65
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
66
+ const data = imageData.data;
67
+ let isAllBlack = true;
68
+ for (let i = 0; i < data.length; i += 4) {
69
+ if (data[i] !== 0 || data[i + 1] !== 0 || data[i + 2] !== 0) {
70
+ isAllBlack = false;
71
+ break;
72
+ }
73
+ }
74
+
75
+ if (isAllBlack) {
76
+ this.appendError('camera appears to be producing only black frames');
77
+ } else {
78
+ this.appendMessage('received video frames');
79
+ }
80
+ resolve();
81
+ }, 1000);
82
+ };
83
+ video.play();
84
+ });
85
+
86
+ video.remove();
87
+ }
36
88
  }
package/src/index.ts CHANGED
@@ -95,6 +95,7 @@ export {
95
95
  Room,
96
96
  SubscriptionError,
97
97
  TrackPublication,
98
+ TrackType,
98
99
  compareVersions,
99
100
  createAudioAnalyser,
100
101
  getBrowser,
@@ -127,5 +128,4 @@ export type {
127
128
  VideoSenderStats,
128
129
  ReconnectContext,
129
130
  ReconnectPolicy,
130
- TrackType,
131
131
  };
@@ -1,5 +1,5 @@
1
1
  import type { DataStream_Chunk } from '@livekit/protocol';
2
- import type { BaseStreamInfo, ByteStreamInfo, TextStreamChunk, TextStreamInfo } from './types';
2
+ import type { BaseStreamInfo, ByteStreamInfo, TextStreamInfo } from './types';
3
3
  import { bigIntToNumber } from './utils';
4
4
 
5
5
  abstract class BaseStreamReader<T extends BaseStreamInfo> {
@@ -124,7 +124,7 @@ export class TextStreamReader extends BaseStreamReader<TextStreamInfo> {
124
124
  const decoder = new TextDecoder();
125
125
 
126
126
  return {
127
- next: async (): Promise<IteratorResult<TextStreamChunk>> => {
127
+ next: async (): Promise<IteratorResult<string>> => {
128
128
  try {
129
129
  const { done, value } = await reader.read();
130
130
  if (done) {
@@ -134,14 +134,7 @@ export class TextStreamReader extends BaseStreamReader<TextStreamInfo> {
134
134
 
135
135
  return {
136
136
  done: false,
137
- value: {
138
- index: bigIntToNumber(value.chunkIndex),
139
- current: decoder.decode(value.content),
140
- collected: Array.from(this.receivedChunks.values())
141
- .sort((a, b) => bigIntToNumber(a.chunkIndex) - bigIntToNumber(b.chunkIndex))
142
- .map((chunk) => decoder.decode(chunk.content))
143
- .join(''),
144
- },
137
+ value: decoder.decode(value.content),
145
138
  };
146
139
  }
147
140
  } catch (error) {
@@ -150,7 +143,7 @@ export class TextStreamReader extends BaseStreamReader<TextStreamInfo> {
150
143
  }
151
144
  },
152
145
 
153
- async return(): Promise<IteratorResult<TextStreamChunk>> {
146
+ async return(): Promise<IteratorResult<string>> {
154
147
  reader.releaseLock();
155
148
  return { done: true, value: undefined };
156
149
  },
@@ -158,11 +151,11 @@ export class TextStreamReader extends BaseStreamReader<TextStreamInfo> {
158
151
  }
159
152
 
160
153
  async readAll(): Promise<string> {
161
- let latestString: string = '';
162
- for await (const { collected } of this) {
163
- latestString = collected;
154
+ let finalString: string = '';
155
+ for await (const chunk of this) {
156
+ finalString += chunk;
164
157
  }
165
- return latestString;
158
+ return finalString;
166
159
  }
167
160
  }
168
161
 
@@ -1,15 +1,15 @@
1
1
  import type { BaseStreamInfo, ByteStreamInfo, TextStreamInfo } from './types';
2
2
 
3
3
  class BaseStreamWriter<T, InfoType extends BaseStreamInfo> {
4
- protected writableStream: WritableStream<[T, number?]>;
4
+ protected writableStream: WritableStream<T>;
5
5
 
6
- protected defaultWriter: WritableStreamDefaultWriter<[T, number?]>;
6
+ protected defaultWriter: WritableStreamDefaultWriter<T>;
7
7
 
8
8
  protected onClose?: () => void;
9
9
 
10
10
  readonly info: InfoType;
11
11
 
12
- constructor(writableStream: WritableStream<[T, number?]>, info: InfoType, onClose?: () => void) {
12
+ constructor(writableStream: WritableStream<T>, info: InfoType, onClose?: () => void) {
13
13
  this.writableStream = writableStream;
14
14
  this.defaultWriter = writableStream.getWriter();
15
15
  this.onClose = onClose;
@@ -17,7 +17,7 @@ class BaseStreamWriter<T, InfoType extends BaseStreamInfo> {
17
17
  }
18
18
 
19
19
  write(chunk: T): Promise<void> {
20
- return this.defaultWriter.write([chunk]);
20
+ return this.defaultWriter.write(chunk);
21
21
  }
22
22
 
23
23
  async close() {
@@ -90,6 +90,7 @@ import {
90
90
  isWeb,
91
91
  numberToBigInt,
92
92
  sleep,
93
+ splitUtf8,
93
94
  supportsAV1,
94
95
  supportsVP9,
95
96
  } from '../utils';
@@ -1536,19 +1537,9 @@ export default class LocalParticipant extends Participant {
1536
1537
  attachedStreamIds: fileIds,
1537
1538
  });
1538
1539
 
1539
- 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
1540
- const totalTextChunks = Math.ceil(totalTextLength / textChunkSize);
1541
-
1542
- for (let i = 0; i < totalTextChunks; i++) {
1543
- const chunkData = text.slice(
1544
- i * textChunkSize,
1545
- Math.min((i + 1) * textChunkSize, totalTextLength),
1546
- );
1547
- await this.engine.waitForBufferStatusLow(DataPacket_Kind.RELIABLE);
1548
- await writer.write(chunkData);
1549
-
1550
- handleProgress(Math.ceil((i + 1) / totalTextChunks), 0);
1551
- }
1540
+ await writer.write(text);
1541
+ // set text part of progress to 1
1542
+ handleProgress(1, 0);
1552
1543
 
1553
1544
  await writer.close();
1554
1545
 
@@ -1614,20 +1605,13 @@ export default class LocalParticipant extends Participant {
1614
1605
  let chunkId = 0;
1615
1606
  const localP = this;
1616
1607
 
1617
- const writableStream = new WritableStream<[string, number?]>({
1608
+ const writableStream = new WritableStream<string>({
1618
1609
  // Implement the sink
1619
- write([textChunk]) {
1620
- const textInBytes = new TextEncoder().encode(textChunk);
1621
-
1622
- if (textInBytes.byteLength > STREAM_CHUNK_SIZE) {
1623
- this.abort?.();
1624
- throw new Error('chunk size too large');
1625
- }
1626
-
1627
- return new Promise(async (resolve) => {
1610
+ async write(text) {
1611
+ for (const textChunk in splitUtf8(text, STREAM_CHUNK_SIZE)) {
1628
1612
  await localP.engine.waitForBufferStatusLow(DataPacket_Kind.RELIABLE);
1629
1613
  const chunk = new DataStream_Chunk({
1630
- content: textInBytes,
1614
+ content: new TextEncoder().encode(textChunk),
1631
1615
  streamId,
1632
1616
  chunkIndex: numberToBigInt(chunkId),
1633
1617
  });
@@ -1641,8 +1625,7 @@ export default class LocalParticipant extends Participant {
1641
1625
  await localP.engine.sendDataPacket(chunkPacket, DataPacket_Kind.RELIABLE);
1642
1626
 
1643
1627
  chunkId += 1;
1644
- resolve();
1645
- });
1628
+ }
1646
1629
  },
1647
1630
  async close() {
1648
1631
  const trailer = new DataStream_Trailer({
@@ -256,6 +256,10 @@ export function computeTrackBackupEncodings(
256
256
  const width = settings.width ?? track.dimensions?.width;
257
257
  const height = settings.height ?? track.dimensions?.height;
258
258
 
259
+ // disable simulcast for screenshare backup codec since L1Tx is used by primary codec
260
+ if (track.source === Track.Source.ScreenShare && opts.simulcast) {
261
+ opts.simulcast = false;
262
+ }
259
263
  const encodings = computeVideoEncodings(
260
264
  track.source === Track.Source.ScreenShare,
261
265
  width,
@@ -232,6 +232,7 @@ export default abstract class LocalTrack<
232
232
  ) {
233
233
  return true;
234
234
  }
235
+
235
236
  this._constraints.deviceId = deviceId;
236
237
 
237
238
  // when track is muted, underlying media stream track is stopped and
@@ -313,6 +314,7 @@ export default abstract class LocalTrack<
313
314
  if (!constraints) {
314
315
  constraints = this._constraints;
315
316
  }
317
+ const { deviceId, ...otherConstraints } = this._constraints;
316
318
  this.log.debug('restarting track with constraints', { ...this.logContext, constraints });
317
319
 
318
320
  const streamConstraints: MediaStreamConstraints = {
@@ -321,9 +323,9 @@ export default abstract class LocalTrack<
321
323
  };
322
324
 
323
325
  if (this.kind === Track.Kind.Video) {
324
- streamConstraints.video = constraints;
326
+ streamConstraints.video = deviceId ? { deviceId } : true;
325
327
  } else {
326
- streamConstraints.audio = constraints;
328
+ streamConstraints.audio = deviceId ? { deviceId } : true;
327
329
  }
328
330
 
329
331
  // these steps are duplicated from setMediaStreamTrack because we must stop
@@ -340,6 +342,7 @@ export default abstract class LocalTrack<
340
342
  // create new track and attach
341
343
  const mediaStream = await navigator.mediaDevices.getUserMedia(streamConstraints);
342
344
  const newTrack = mediaStream.getTracks()[0];
345
+ await newTrack.applyConstraints(otherConstraints);
343
346
  newTrack.addEventListener('ended', this.handleEnded);
344
347
  this.log.debug('re-acquired MediaStreamTrack', this.logContext);
345
348