livekit-client 1.4.4 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. package/dist/livekit-client.esm.mjs +510 -38
  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 +25 -0
  6. package/dist/src/connectionHelper/ConnectionCheck.d.ts.map +1 -0
  7. package/dist/src/connectionHelper/checks/Checker.d.ts +59 -0
  8. package/dist/src/connectionHelper/checks/Checker.d.ts.map +1 -0
  9. package/dist/src/connectionHelper/checks/publishAudio.d.ts +6 -0
  10. package/dist/src/connectionHelper/checks/publishAudio.d.ts.map +1 -0
  11. package/dist/src/connectionHelper/checks/publishVideo.d.ts +6 -0
  12. package/dist/src/connectionHelper/checks/publishVideo.d.ts.map +1 -0
  13. package/dist/src/connectionHelper/checks/reconnect.d.ts +6 -0
  14. package/dist/src/connectionHelper/checks/reconnect.d.ts.map +1 -0
  15. package/dist/src/connectionHelper/checks/turn.d.ts +6 -0
  16. package/dist/src/connectionHelper/checks/turn.d.ts.map +1 -0
  17. package/dist/src/connectionHelper/checks/webrtc.d.ts +6 -0
  18. package/dist/src/connectionHelper/checks/webrtc.d.ts.map +1 -0
  19. package/dist/src/connectionHelper/checks/websocket.d.ts +6 -0
  20. package/dist/src/connectionHelper/checks/websocket.d.ts.map +1 -0
  21. package/dist/src/index.d.ts +3 -1
  22. package/dist/src/index.d.ts.map +1 -1
  23. package/dist/src/proto/livekit_rtc.d.ts +8 -0
  24. package/dist/src/proto/livekit_rtc.d.ts.map +1 -1
  25. package/dist/src/room/DeviceManager.d.ts.map +1 -1
  26. package/dist/src/room/Room.d.ts +6 -0
  27. package/dist/src/room/Room.d.ts.map +1 -1
  28. package/dist/src/room/events.d.ts +5 -1
  29. package/dist/src/room/events.d.ts.map +1 -1
  30. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  31. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  32. package/dist/src/room/track/RemoteTrackPublication.d.ts +2 -0
  33. package/dist/src/room/track/RemoteTrackPublication.d.ts.map +1 -1
  34. package/dist/ts4.2/src/api/SignalClient.d.ts +85 -0
  35. package/dist/ts4.2/src/connectionHelper/ConnectionCheck.d.ts +25 -0
  36. package/dist/ts4.2/src/connectionHelper/checks/Checker.d.ts +59 -0
  37. package/dist/ts4.2/src/connectionHelper/checks/publishAudio.d.ts +6 -0
  38. package/dist/ts4.2/src/connectionHelper/checks/publishVideo.d.ts +6 -0
  39. package/dist/ts4.2/src/connectionHelper/checks/reconnect.d.ts +6 -0
  40. package/dist/ts4.2/src/connectionHelper/checks/turn.d.ts +6 -0
  41. package/dist/ts4.2/src/connectionHelper/checks/webrtc.d.ts +6 -0
  42. package/dist/ts4.2/src/connectionHelper/checks/websocket.d.ts +6 -0
  43. package/dist/ts4.2/src/index.d.ts +30 -0
  44. package/dist/ts4.2/src/logger.d.ts +26 -0
  45. package/dist/ts4.2/src/options.d.ts +91 -0
  46. package/dist/ts4.2/src/proto/google/protobuf/timestamp.d.ts +141 -0
  47. package/dist/ts4.2/src/proto/livekit_models.d.ts +1421 -0
  48. package/dist/ts4.2/src/proto/livekit_rtc.d.ts +7122 -0
  49. package/dist/ts4.2/src/room/DefaultReconnectPolicy.d.ts +8 -0
  50. package/dist/ts4.2/src/room/DeviceManager.d.ts +9 -0
  51. package/dist/ts4.2/src/room/PCTransport.d.ts +33 -0
  52. package/dist/ts4.2/src/room/RTCEngine.d.ts +96 -0
  53. package/dist/ts4.2/src/room/ReconnectPolicy.d.ts +23 -0
  54. package/dist/ts4.2/src/room/Room.d.ts +203 -0
  55. package/dist/ts4.2/src/room/defaults.d.ts +8 -0
  56. package/dist/ts4.2/src/room/errors.d.ts +39 -0
  57. package/dist/ts4.2/src/room/events.d.ts +422 -0
  58. package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +141 -0
  59. package/dist/ts4.2/src/room/participant/Participant.d.ts +92 -0
  60. package/dist/ts4.2/src/room/participant/ParticipantTrackPermission.d.ts +26 -0
  61. package/dist/ts4.2/src/room/participant/RemoteParticipant.d.ts +52 -0
  62. package/dist/ts4.2/src/room/participant/publishUtils.d.ts +19 -0
  63. package/dist/ts4.2/src/room/stats.d.ts +67 -0
  64. package/dist/ts4.2/src/room/track/LocalAudioTrack.d.ts +25 -0
  65. package/dist/ts4.2/src/room/track/LocalTrack.d.ts +42 -0
  66. package/dist/ts4.2/src/room/track/LocalTrackPublication.d.ts +38 -0
  67. package/dist/ts4.2/src/room/track/LocalVideoTrack.d.ts +53 -0
  68. package/dist/ts4.2/src/room/track/RemoteAudioTrack.d.ts +53 -0
  69. package/dist/ts4.2/src/room/track/RemoteTrack.d.ts +15 -0
  70. package/dist/ts4.2/src/room/track/RemoteTrackPublication.d.ts +61 -0
  71. package/dist/ts4.2/src/room/track/RemoteVideoTrack.d.ts +52 -0
  72. package/dist/ts4.2/src/room/track/Track.d.ts +121 -0
  73. package/dist/ts4.2/src/room/track/TrackPublication.d.ts +68 -0
  74. package/dist/ts4.2/src/room/track/create.d.ts +24 -0
  75. package/dist/ts4.2/src/room/track/options.d.ts +241 -0
  76. package/dist/ts4.2/src/room/track/types.d.ts +23 -0
  77. package/dist/ts4.2/src/room/track/utils.d.ts +14 -0
  78. package/dist/ts4.2/src/room/utils.d.ts +35 -0
  79. package/dist/ts4.2/src/test/MockMediaStreamTrack.d.ts +26 -0
  80. package/dist/ts4.2/src/test/mocks.d.ts +11 -0
  81. package/dist/ts4.2/src/version.d.ts +3 -0
  82. package/package.json +13 -3
  83. package/src/api/SignalClient.ts +2 -2
  84. package/src/connectionHelper/ConnectionCheck.ts +90 -0
  85. package/src/connectionHelper/checks/Checker.ts +164 -0
  86. package/src/connectionHelper/checks/publishAudio.ts +33 -0
  87. package/src/connectionHelper/checks/publishVideo.ts +33 -0
  88. package/src/connectionHelper/checks/reconnect.ts +45 -0
  89. package/src/connectionHelper/checks/turn.ts +53 -0
  90. package/src/connectionHelper/checks/webrtc.ts +18 -0
  91. package/src/connectionHelper/checks/websocket.ts +22 -0
  92. package/src/index.ts +3 -1
  93. package/src/proto/livekit_rtc.ts +12 -1
  94. package/src/room/DeviceManager.ts +0 -17
  95. package/src/room/Room.ts +22 -2
  96. package/src/room/events.ts +5 -0
  97. package/src/room/participant/LocalParticipant.ts +15 -8
  98. package/src/room/track/LocalTrack.ts +3 -0
  99. package/src/room/track/RemoteTrackPublication.ts +20 -0
@@ -0,0 +1,164 @@
1
+ import { EventEmitter } from 'events';
2
+ import type TypedEmitter from 'typed-emitter';
3
+ import type { RoomConnectOptions, RoomOptions } from '../../options';
4
+ import Room, { ConnectionState } from '../../room/Room';
5
+ import type RTCEngine from '../../room/RTCEngine';
6
+
7
+ type LogMessage = {
8
+ level: 'info' | 'warning' | 'error';
9
+ message: string;
10
+ };
11
+
12
+ export enum CheckStatus {
13
+ IDLE,
14
+ RUNNING,
15
+ SKIPPED,
16
+ SUCCESS,
17
+ FAILED,
18
+ }
19
+
20
+ export type CheckInfo = {
21
+ name: string;
22
+ logs: Array<LogMessage>;
23
+ status: CheckStatus;
24
+ description: string;
25
+ };
26
+
27
+ export interface CheckerOptions {
28
+ errorsAsWarnings?: boolean;
29
+ roomOptions?: RoomOptions;
30
+ connectOptions?: RoomConnectOptions;
31
+ }
32
+
33
+ export abstract class Checker extends (EventEmitter as new () => TypedEmitter<CheckerCallbacks>) {
34
+ protected url: string;
35
+
36
+ protected token: string;
37
+
38
+ room: Room;
39
+
40
+ connectOptions?: RoomConnectOptions;
41
+
42
+ status: CheckStatus = CheckStatus.IDLE;
43
+
44
+ logs: Array<LogMessage> = [];
45
+
46
+ errorsAsWarnings: boolean = false;
47
+
48
+ name: string;
49
+
50
+ constructor(url: string, token: string, options: CheckerOptions = {}) {
51
+ super();
52
+ this.url = url;
53
+ this.token = token;
54
+ this.name = this.constructor.name;
55
+ this.room = new Room(options.roomOptions);
56
+ this.connectOptions = options.connectOptions;
57
+ if (options.errorsAsWarnings) {
58
+ this.errorsAsWarnings = options.errorsAsWarnings;
59
+ }
60
+ }
61
+
62
+ abstract get description(): string;
63
+
64
+ protected abstract perform(): Promise<void>;
65
+
66
+ async run(onComplete?: () => void) {
67
+ if (this.status !== CheckStatus.IDLE) {
68
+ throw Error('check is running already');
69
+ }
70
+ this.setStatus(CheckStatus.RUNNING);
71
+ this.appendMessage(`${this.name} started.`);
72
+
73
+ try {
74
+ await this.perform();
75
+ } catch (err) {
76
+ if (err instanceof Error) {
77
+ if (this.errorsAsWarnings) {
78
+ this.appendWarning(err.message);
79
+ } else {
80
+ this.appendError(err.message);
81
+ }
82
+ }
83
+ }
84
+
85
+ await this.disconnect();
86
+
87
+ // sleep for a bit to ensure disconnect
88
+ await new Promise((resolve) => setTimeout(resolve, 500));
89
+
90
+ // @ts-ignore
91
+ if (this.status !== CheckStatus.SKIPPED) {
92
+ this.setStatus(this.isSuccess() ? CheckStatus.SUCCESS : CheckStatus.FAILED);
93
+ }
94
+
95
+ if (onComplete) {
96
+ onComplete();
97
+ }
98
+ return this.getInfo();
99
+ }
100
+
101
+ protected isSuccess(): boolean {
102
+ return !this.logs.some((l) => l.level === 'error');
103
+ }
104
+
105
+ protected async connect(): Promise<Room> {
106
+ if (this.room.state === ConnectionState.Connected) {
107
+ return this.room;
108
+ }
109
+ await this.room.connect(this.url, this.token);
110
+ return this.room;
111
+ }
112
+
113
+ protected async disconnect() {
114
+ if (this.room && this.room.state !== ConnectionState.Disconnected) {
115
+ await this.room.disconnect();
116
+ // wait for it to go through
117
+ await new Promise((resolve) => setTimeout(resolve, 500));
118
+ }
119
+ }
120
+
121
+ protected skip() {
122
+ this.setStatus(CheckStatus.SKIPPED);
123
+ }
124
+
125
+ protected appendMessage(message: string) {
126
+ this.logs.push({ level: 'info', message });
127
+ this.emit('update', this.getInfo());
128
+ }
129
+
130
+ protected appendWarning(message: string) {
131
+ this.logs.push({ level: 'warning', message });
132
+ this.emit('update', this.getInfo());
133
+ }
134
+
135
+ protected appendError(message: string) {
136
+ this.logs.push({ level: 'error', message });
137
+ this.emit('update', this.getInfo());
138
+ }
139
+
140
+ protected setStatus(status: CheckStatus) {
141
+ this.status = status;
142
+ this.emit('update', this.getInfo());
143
+ }
144
+
145
+ protected get engine(): RTCEngine | undefined {
146
+ return this.room?.engine;
147
+ }
148
+
149
+ getInfo(): CheckInfo {
150
+ return {
151
+ logs: this.logs,
152
+ name: this.name,
153
+ status: this.status,
154
+ description: this.description,
155
+ };
156
+ }
157
+ }
158
+ export type InstantiableCheck<T extends Checker> = {
159
+ new (url: string, token: string, options?: CheckerOptions): T;
160
+ };
161
+
162
+ type CheckerCallbacks = {
163
+ update: (info: CheckInfo) => void;
164
+ };
@@ -0,0 +1,33 @@
1
+ import { createLocalAudioTrack } from '../../room/track/create';
2
+ import { Checker } from './Checker';
3
+
4
+ export class PublishAudioCheck extends Checker {
5
+ get description(): string {
6
+ return 'Can publish audio';
7
+ }
8
+
9
+ async perform(): Promise<void> {
10
+ const room = await this.connect();
11
+
12
+ const track = await createLocalAudioTrack();
13
+ room.localParticipant.publishTrack(track);
14
+ // wait for a few seconds to publish
15
+ await new Promise((resolve) => setTimeout(resolve, 3000));
16
+
17
+ // verify RTC stats that it's publishing
18
+ const stats = await track.sender?.getStats();
19
+ if (!stats) {
20
+ throw new Error('Could not get RTCStats');
21
+ }
22
+ let numPackets = 0;
23
+ stats.forEach((stat) => {
24
+ if (stat.type === 'outbound-rtp' && stat.mediaType === 'audio') {
25
+ numPackets = stat.packetsSent;
26
+ }
27
+ });
28
+ if (numPackets === 0) {
29
+ throw new Error('Could not determine packets are sent');
30
+ }
31
+ this.appendMessage(`published ${numPackets} audio packets`);
32
+ }
33
+ }
@@ -0,0 +1,33 @@
1
+ import { createLocalVideoTrack } from '../../room/track/create';
2
+ import { Checker } from './Checker';
3
+
4
+ export class PublishVideoCheck extends Checker {
5
+ get description(): string {
6
+ return 'Can publish video';
7
+ }
8
+
9
+ async perform(): Promise<void> {
10
+ const room = await this.connect();
11
+
12
+ const track = await createLocalVideoTrack();
13
+ room.localParticipant.publishTrack(track);
14
+ // wait for a few seconds to publish
15
+ await new Promise((resolve) => setTimeout(resolve, 3000));
16
+
17
+ // verify RTC stats that it's publishing
18
+ const stats = await track.sender?.getStats();
19
+ if (!stats) {
20
+ throw new Error('Could not get RTCStats');
21
+ }
22
+ let numPackets = 0;
23
+ stats.forEach((stat) => {
24
+ if (stat.type === 'outbound-rtp' && stat.mediaType === 'video') {
25
+ numPackets = stat.packetsSent;
26
+ }
27
+ });
28
+ if (numPackets === 0) {
29
+ throw new Error('Could not determine packets are sent');
30
+ }
31
+ this.appendMessage(`published ${numPackets} video packets`);
32
+ }
33
+ }
@@ -0,0 +1,45 @@
1
+ import { RoomEvent } from '../../room/events';
2
+ import { ConnectionState } from '../../room/Room';
3
+ import { Checker } from './Checker';
4
+
5
+ export class ReconnectCheck extends Checker {
6
+ get description(): string {
7
+ return 'Resuming connection after interruption';
8
+ }
9
+
10
+ async perform(): Promise<void> {
11
+ const room = await this.connect();
12
+ let reconnectingTriggered = false;
13
+ let reconnected = false;
14
+
15
+ let reconnectResolver: (value: unknown) => void;
16
+ const reconnectTimeout = new Promise((resolve) => {
17
+ setTimeout(resolve, 5000);
18
+ reconnectResolver = resolve;
19
+ });
20
+
21
+ room
22
+ .on(RoomEvent.Reconnecting, () => {
23
+ reconnectingTriggered = true;
24
+ })
25
+ .on(RoomEvent.Reconnected, () => {
26
+ reconnected = true;
27
+ reconnectResolver(true);
28
+ });
29
+
30
+ room.engine.client.ws?.close();
31
+ const onClose = room.engine.client.onClose;
32
+ if (onClose) {
33
+ onClose('');
34
+ }
35
+
36
+ await reconnectTimeout;
37
+
38
+ if (!reconnectingTriggered) {
39
+ throw new Error('Did not attempt to reconnect');
40
+ } else if (!reconnected || room.state !== ConnectionState.Connected) {
41
+ this.appendWarning('reconnection is only possible in Redis-based configurations');
42
+ throw new Error('Not able to reconnect');
43
+ }
44
+ }
45
+ }
@@ -0,0 +1,53 @@
1
+ import { SignalClient } from '../../api/SignalClient';
2
+ import { Checker } from './Checker';
3
+
4
+ export class TURNCheck extends Checker {
5
+ get description(): string {
6
+ return 'Can connect via TURN';
7
+ }
8
+
9
+ async perform(): Promise<void> {
10
+ const signalClient = new SignalClient();
11
+ const joinRes = await signalClient.join(this.url, this.token, {
12
+ autoSubscribe: true,
13
+ maxRetries: 0,
14
+ });
15
+
16
+ let hasTLS = false;
17
+ let hasTURN = false;
18
+ let hasSTUN = false;
19
+
20
+ for (let iceServer of joinRes.iceServers) {
21
+ for (let url of iceServer.urls) {
22
+ if (url.startsWith('turn:')) {
23
+ hasTURN = true;
24
+ hasSTUN = true;
25
+ } else if (url.startsWith('turns:')) {
26
+ hasTURN = true;
27
+ hasSTUN = true;
28
+ hasTLS = true;
29
+ }
30
+ if (url.startsWith('stun:')) {
31
+ hasSTUN = true;
32
+ }
33
+ }
34
+ }
35
+ if (!hasSTUN) {
36
+ this.appendWarning('No STUN servers configured on server side.');
37
+ } else if (hasTURN && !hasTLS) {
38
+ this.appendWarning('TURN is configured server side, but TURN/TLS is unavailable.');
39
+ }
40
+ signalClient.close();
41
+ if (this.connectOptions?.rtcConfig?.iceServers || hasTURN) {
42
+ await this.room!.connect(this.url, this.token, {
43
+ rtcConfig: {
44
+ iceTransportPolicy: 'relay',
45
+ },
46
+ });
47
+ } else {
48
+ this.appendWarning('No TURN servers configured.');
49
+ this.skip();
50
+ await new Promise((resolve) => setTimeout(resolve, 0));
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,18 @@
1
+ import { Checker } from './Checker';
2
+
3
+ export class WebRTCCheck extends Checker {
4
+ get description(): string {
5
+ return 'Establishing WebRTC connection';
6
+ }
7
+
8
+ protected async perform(): Promise<void> {
9
+ try {
10
+ console.log('initiating room connection');
11
+ this.room = await this.connect();
12
+ console.log('now the room is connected');
13
+ } catch (err) {
14
+ this.appendWarning('ports need to be open on firewall in order to connect.');
15
+ throw err;
16
+ }
17
+ }
18
+ }
@@ -0,0 +1,22 @@
1
+ import { SignalClient } from '../../api/SignalClient';
2
+ import { Checker } from './Checker';
3
+
4
+ export class WebSocketCheck extends Checker {
5
+ get description(): string {
6
+ return 'Connecting to signal connection via WebSocket';
7
+ }
8
+
9
+ protected async perform(): Promise<void> {
10
+ if (this.url.startsWith('ws:') || this.url.startsWith('http:')) {
11
+ this.appendWarning('Server is insecure, clients may block connections to it');
12
+ }
13
+
14
+ let signalClient = new SignalClient();
15
+ const joinRes = await signalClient.join(this.url, this.token, {
16
+ autoSubscribe: true,
17
+ maxRetries: 0,
18
+ });
19
+ this.appendMessage(`Connected to server, version ${joinRes.serverVersion}.`);
20
+ signalClient.close();
21
+ }
22
+ }
package/src/index.ts CHANGED
@@ -13,7 +13,8 @@ import LocalVideoTrack from './room/track/LocalVideoTrack';
13
13
  import RemoteAudioTrack from './room/track/RemoteAudioTrack';
14
14
  import RemoteTrack from './room/track/RemoteTrack';
15
15
  import RemoteTrackPublication from './room/track/RemoteTrackPublication';
16
- import RemoteVideoTrack, { type ElementInfo } from './room/track/RemoteVideoTrack';
16
+ import RemoteVideoTrack from './room/track/RemoteVideoTrack';
17
+ import type { ElementInfo } from './room/track/RemoteVideoTrack';
17
18
  import { TrackPublication } from './room/track/TrackPublication';
18
19
  import {
19
20
  getEmptyAudioStreamTrack,
@@ -32,6 +33,7 @@ export * from './room/track/options';
32
33
  export * from './room/track/Track';
33
34
  export * from './room/track/types';
34
35
  export * from './version';
36
+ export * from './connectionHelper/ConnectionCheck';
35
37
  export {
36
38
  setLogLevel,
37
39
  setLogExtension,
@@ -267,6 +267,8 @@ export interface UpdateTrackSettings {
267
267
  width: number;
268
268
  /** for video, height to receive */
269
269
  height: number;
270
+ /** for video, frame rate to receive */
271
+ fps: number;
270
272
  }
271
273
 
272
274
  export interface LeaveRequest {
@@ -1843,7 +1845,7 @@ export const UpdateSubscription = {
1843
1845
  };
1844
1846
 
1845
1847
  function createBaseUpdateTrackSettings(): UpdateTrackSettings {
1846
- return { trackSids: [], disabled: false, quality: 0, width: 0, height: 0 };
1848
+ return { trackSids: [], disabled: false, quality: 0, width: 0, height: 0, fps: 0 };
1847
1849
  }
1848
1850
 
1849
1851
  export const UpdateTrackSettings = {
@@ -1863,6 +1865,9 @@ export const UpdateTrackSettings = {
1863
1865
  if (message.height !== 0) {
1864
1866
  writer.uint32(48).uint32(message.height);
1865
1867
  }
1868
+ if (message.fps !== 0) {
1869
+ writer.uint32(56).uint32(message.fps);
1870
+ }
1866
1871
  return writer;
1867
1872
  },
1868
1873
 
@@ -1888,6 +1893,9 @@ export const UpdateTrackSettings = {
1888
1893
  case 6:
1889
1894
  message.height = reader.uint32();
1890
1895
  break;
1896
+ case 7:
1897
+ message.fps = reader.uint32();
1898
+ break;
1891
1899
  default:
1892
1900
  reader.skipType(tag & 7);
1893
1901
  break;
@@ -1903,6 +1911,7 @@ export const UpdateTrackSettings = {
1903
1911
  quality: isSet(object.quality) ? videoQualityFromJSON(object.quality) : 0,
1904
1912
  width: isSet(object.width) ? Number(object.width) : 0,
1905
1913
  height: isSet(object.height) ? Number(object.height) : 0,
1914
+ fps: isSet(object.fps) ? Number(object.fps) : 0,
1906
1915
  };
1907
1916
  },
1908
1917
 
@@ -1917,6 +1926,7 @@ export const UpdateTrackSettings = {
1917
1926
  message.quality !== undefined && (obj.quality = videoQualityToJSON(message.quality));
1918
1927
  message.width !== undefined && (obj.width = Math.round(message.width));
1919
1928
  message.height !== undefined && (obj.height = Math.round(message.height));
1929
+ message.fps !== undefined && (obj.fps = Math.round(message.fps));
1920
1930
  return obj;
1921
1931
  },
1922
1932
 
@@ -1927,6 +1937,7 @@ export const UpdateTrackSettings = {
1927
1937
  message.quality = object.quality ?? 0;
1928
1938
  message.width = object.width ?? 0;
1929
1939
  message.height = object.height ?? 0;
1940
+ message.fps = object.fps ?? 0;
1930
1941
  return message;
1931
1942
  },
1932
1943
  };
@@ -65,23 +65,6 @@ export default class DeviceManager {
65
65
  devices = devices.filter((device) => device.kind === kind);
66
66
  }
67
67
 
68
- // Chrome returns 'default' devices, we would filter them out, but put the default
69
- // device at first
70
- // we would only do this if there are more than 1 device though
71
- if (devices.length > 1 && devices[0].deviceId === defaultId) {
72
- // find another device with matching group id, and move that to 0
73
- const defaultDevice = devices[0];
74
- for (let i = 1; i < devices.length; i += 1) {
75
- if (devices[i].groupId === defaultDevice.groupId) {
76
- const temp = devices[0];
77
- devices[0] = devices[i];
78
- devices[i] = temp;
79
- break;
80
- }
81
- }
82
- return devices.filter((device) => device !== defaultDevice);
83
- }
84
-
85
68
  return devices;
86
69
  }
87
70
 
package/src/room/Room.ts CHANGED
@@ -101,6 +101,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
101
101
  /** options of room */
102
102
  options: InternalRoomOptions;
103
103
 
104
+ private _isRecording: boolean = false;
105
+
104
106
  private identityToSid: Map<string, string>;
105
107
 
106
108
  /** connect options of room */
@@ -332,6 +334,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
332
334
  this.name = joinResponse.room!.name;
333
335
  this.sid = joinResponse.room!.sid;
334
336
  this.metadata = joinResponse.room!.metadata;
337
+ if (this._isRecording !== joinResponse.room!.activeRecording) {
338
+ this._isRecording = joinResponse.room!.activeRecording;
339
+ this.emit(RoomEvent.RecordingStatusChanged, joinResponse.room!.activeRecording);
340
+ }
335
341
  this.emit(RoomEvent.SignalConnected);
336
342
  } catch (err) {
337
343
  this.recreateEngine();
@@ -424,6 +430,13 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
424
430
  this.connectFuture = undefined;
425
431
  }
426
432
 
433
+ /**
434
+ * if the current room has a participant with `recorder: true` in its JWT grant
435
+ **/
436
+ get isRecording() {
437
+ return this._isRecording;
438
+ }
439
+
427
440
  /**
428
441
  * @internal for testing
429
442
  */
@@ -950,8 +963,14 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
950
963
  };
951
964
 
952
965
  private handleRoomUpdate = (r: RoomModel) => {
953
- this.metadata = r.metadata;
954
- this.emitWhenConnected(RoomEvent.RoomMetadataChanged, r.metadata);
966
+ if (this._isRecording !== r.activeRecording) {
967
+ this._isRecording = r.activeRecording;
968
+ this.emit(RoomEvent.RecordingStatusChanged, r.activeRecording);
969
+ }
970
+ if (this.metadata !== r.metadata) {
971
+ this.metadata = r.metadata;
972
+ this.emitWhenConnected(RoomEvent.RoomMetadataChanged, r.metadata);
973
+ }
955
974
  };
956
975
 
957
976
  private handleConnectionQualityUpdate = (update: ConnectionQualityUpdate) => {
@@ -1273,4 +1292,5 @@ export type RoomEventCallbacks = {
1273
1292
  ) => void;
1274
1293
  audioPlaybackChanged: (playing: boolean) => void;
1275
1294
  signalConnected: () => void;
1295
+ recordingStatusChanged: (recording: boolean) => void;
1276
1296
  };
@@ -250,6 +250,11 @@ export enum RoomEvent {
250
250
  * Signal connected, can publish tracks.
251
251
  */
252
252
  SignalConnected = 'signalConnected',
253
+
254
+ /**
255
+ * Recording of a room has started/stopped.
256
+ */
257
+ RecordingStatusChanged = 'recordingStatusChanged',
253
258
  }
254
259
 
255
260
  export enum ParticipantEvent {
@@ -34,7 +34,7 @@ import {
34
34
  } from '../track/options';
35
35
  import { Track } from '../track/Track';
36
36
  import { constraintsForOptions, mergeDefaultOptions } from '../track/utils';
37
- import { isFireFox, isWeb, supportsAV1 } from '../utils';
37
+ import { isFireFox, isSafari, isWeb, supportsAV1 } from '../utils';
38
38
  import Participant from './Participant';
39
39
  import { ParticipantTrackPermission, trackPermissionToProto } from './ParticipantTrackPermission';
40
40
  import {
@@ -243,6 +243,7 @@ export default class LocalParticipant extends Participant {
243
243
  }
244
244
  const publishPromises: Array<Promise<LocalTrackPublication>> = [];
245
245
  for (const localTrack of localTracks) {
246
+ log.info('publishing track', { localTrack });
246
247
  publishPromises.push(this.publishTrack(localTrack, publishOptions));
247
248
  }
248
249
  const publishedTracks = await Promise.all(publishPromises);
@@ -374,14 +375,20 @@ export default class LocalParticipant extends Participant {
374
375
 
375
376
  let videoConstraints: MediaTrackConstraints | boolean = true;
376
377
  if (options.resolution) {
377
- videoConstraints = {
378
- width: options.resolution.width,
379
- height: options.resolution.height,
380
- frameRate: options.resolution.frameRate,
381
- };
378
+ if (isSafari()) {
379
+ videoConstraints = {
380
+ width: { max: options.resolution.width },
381
+ height: { max: options.resolution.height },
382
+ frameRate: options.resolution.frameRate,
383
+ };
384
+ } else {
385
+ videoConstraints = {
386
+ width: { ideal: options.resolution.width },
387
+ height: { ideal: options.resolution.height },
388
+ frameRate: options.resolution.frameRate,
389
+ };
390
+ }
382
391
  }
383
- // typescript definition is missing getDisplayMedia: https://github.com/microsoft/TypeScript/issues/33232
384
- // @ts-ignore
385
392
  const stream: MediaStream = await navigator.mediaDevices.getDisplayMedia({
386
393
  audio: options.audio ?? false,
387
394
  video: videoConstraints,
@@ -121,6 +121,9 @@ export default abstract class LocalTrack extends Track {
121
121
  }
122
122
  this._mediaStreamTrack = track;
123
123
 
124
+ // sync muted state with the enabled state of the newly provided track
125
+ this._mediaStreamTrack.enabled = !this.isMuted;
126
+
124
127
  await this.resumeUpstream();
125
128
 
126
129
  this.attachedElements.forEach((el) => {
@@ -22,6 +22,8 @@ export default class RemoteTrackPublication extends TrackPublication {
22
22
 
23
23
  protected videoDimensions?: Track.Dimensions;
24
24
 
25
+ protected fps?: number;
26
+
25
27
  constructor(kind: Track.Kind, id: string, name: string, autoSubscribe: boolean | undefined) {
26
28
  super(kind, id, name);
27
29
  this.subscribed = autoSubscribe;
@@ -143,6 +145,23 @@ export default class RemoteTrackPublication extends TrackPublication {
143
145
  this.emitTrackUpdate();
144
146
  }
145
147
 
148
+ setVideoFPS(fps: number) {
149
+ if (!this.isManualOperationAllowed()) {
150
+ return;
151
+ }
152
+
153
+ if (!(this.track instanceof RemoteVideoTrack)) {
154
+ return;
155
+ }
156
+
157
+ if (this.fps === fps) {
158
+ return;
159
+ }
160
+
161
+ this.fps = fps;
162
+ this.emitTrackUpdate();
163
+ }
164
+
146
165
  get videoQuality(): VideoQuality | undefined {
147
166
  return this.currentVideoQuality;
148
167
  }
@@ -256,6 +275,7 @@ export default class RemoteTrackPublication extends TrackPublication {
256
275
  const settings: UpdateTrackSettings = UpdateTrackSettings.fromPartial({
257
276
  trackSids: [this.trackSid],
258
277
  disabled: this.disabled,
278
+ fps: this.fps,
259
279
  });
260
280
  if (this.videoDimensions) {
261
281
  settings.width = this.videoDimensions.width;