livekit-client 2.9.0 → 2.9.2

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.
Files changed (55) hide show
  1. package/dist/livekit-client.esm.mjs +393 -75
  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 +4 -4
  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 +4 -4
  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 +9 -16
  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> {
@@ -59,7 +59,7 @@ export class ByteStreamReader extends BaseStreamReader<ByteStreamInfo> {
59
59
  }
60
60
  },
61
61
 
62
- return(): IteratorResult<Uint8Array> {
62
+ async return(): Promise<IteratorResult<Uint8Array>> {
63
63
  reader.releaseLock();
64
64
  return { done: true, value: undefined };
65
65
  },
@@ -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
- return(): 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