livekit-client 0.14.0 → 0.15.0

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 (94) hide show
  1. package/.github/workflows/{lint.yaml → test.yaml} +7 -4
  2. package/README.md +27 -12
  3. package/dist/api/SignalClient.d.ts +4 -31
  4. package/dist/api/SignalClient.js +25 -7
  5. package/dist/api/SignalClient.js.map +1 -1
  6. package/dist/connect.d.ts +1 -1
  7. package/dist/connect.js +70 -56
  8. package/dist/connect.js.map +1 -1
  9. package/dist/index.d.ts +3 -4
  10. package/dist/index.js +4 -4
  11. package/dist/index.js.map +1 -1
  12. package/dist/logger.d.ts +10 -0
  13. package/dist/logger.js +14 -0
  14. package/dist/logger.js.map +1 -1
  15. package/dist/options.d.ts +77 -12
  16. package/dist/options.js +0 -10
  17. package/dist/options.js.map +1 -1
  18. package/dist/proto/livekit_models.d.ts +28 -1
  19. package/dist/proto/livekit_models.js +194 -3
  20. package/dist/proto/livekit_models.js.map +1 -1
  21. package/dist/proto/livekit_rtc.d.ts +49 -7
  22. package/dist/proto/livekit_rtc.js +303 -28
  23. package/dist/proto/livekit_rtc.js.map +1 -1
  24. package/dist/room/RTCEngine.d.ts +2 -2
  25. package/dist/room/RTCEngine.js +6 -4
  26. package/dist/room/RTCEngine.js.map +1 -1
  27. package/dist/room/Room.d.ts +12 -14
  28. package/dist/room/Room.js +78 -45
  29. package/dist/room/Room.js.map +1 -1
  30. package/dist/room/events.d.ts +98 -1
  31. package/dist/room/events.js +97 -0
  32. package/dist/room/events.js.map +1 -1
  33. package/dist/room/participant/LocalParticipant.d.ts +27 -9
  34. package/dist/room/participant/LocalParticipant.js +194 -155
  35. package/dist/room/participant/LocalParticipant.js.map +1 -1
  36. package/dist/room/participant/Participant.js +6 -2
  37. package/dist/room/participant/Participant.js.map +1 -1
  38. package/dist/room/participant/RemoteParticipant.d.ts +1 -1
  39. package/dist/room/participant/RemoteParticipant.js +6 -5
  40. package/dist/room/participant/RemoteParticipant.js.map +1 -1
  41. package/dist/room/participant/publishUtils.d.ts +11 -0
  42. package/dist/room/participant/publishUtils.js +148 -0
  43. package/dist/room/participant/publishUtils.js.map +1 -0
  44. package/dist/room/participant/publishUtils.test.d.ts +1 -0
  45. package/dist/room/participant/publishUtils.test.js +79 -0
  46. package/dist/room/participant/publishUtils.test.js.map +1 -0
  47. package/dist/room/track/LocalAudioTrack.d.ts +4 -3
  48. package/dist/room/track/LocalAudioTrack.js +5 -3
  49. package/dist/room/track/LocalAudioTrack.js.map +1 -1
  50. package/dist/room/track/LocalTrack.d.ts +1 -3
  51. package/dist/room/track/LocalTrack.js +2 -49
  52. package/dist/room/track/LocalTrack.js.map +1 -1
  53. package/dist/room/track/LocalVideoTrack.d.ts +6 -4
  54. package/dist/room/track/LocalVideoTrack.js +41 -12
  55. package/dist/room/track/LocalVideoTrack.js.map +1 -1
  56. package/dist/room/track/LocalVideoTrack.test.d.ts +1 -0
  57. package/dist/room/track/LocalVideoTrack.test.js +68 -0
  58. package/dist/room/track/LocalVideoTrack.test.js.map +1 -0
  59. package/dist/room/track/RemoteTrackPublication.d.ts +10 -4
  60. package/dist/room/track/RemoteTrackPublication.js +60 -4
  61. package/dist/room/track/RemoteTrackPublication.js.map +1 -1
  62. package/dist/room/track/RemoteVideoTrack.d.ts +15 -1
  63. package/dist/room/track/RemoteVideoTrack.js +98 -1
  64. package/dist/room/track/RemoteVideoTrack.js.map +1 -1
  65. package/dist/room/track/Track.d.ts +15 -2
  66. package/dist/room/track/Track.js +6 -2
  67. package/dist/room/track/Track.js.map +1 -1
  68. package/dist/room/track/options.d.ts +15 -65
  69. package/dist/room/track/options.js +14 -13
  70. package/dist/room/track/options.js.map +1 -1
  71. package/dist/room/track/utils.d.ts +3 -0
  72. package/dist/room/track/utils.js +68 -0
  73. package/dist/room/track/utils.js.map +1 -0
  74. package/dist/room/track/utils.test.d.ts +1 -0
  75. package/dist/room/track/utils.test.js +85 -0
  76. package/dist/room/track/utils.test.js.map +1 -0
  77. package/dist/room/utils.d.ts +6 -0
  78. package/dist/room/utils.js +25 -1
  79. package/dist/room/utils.js.map +1 -1
  80. package/dist/version.d.ts +1 -1
  81. package/dist/version.js +1 -1
  82. package/example/index.html +183 -178
  83. package/example/sample.ts +410 -326
  84. package/example/styles.css +144 -0
  85. package/example/webpack.config.js +1 -1
  86. package/jest.config.js +6 -0
  87. package/package.json +9 -6
  88. package/tsconfig.eslint.json +8 -1
  89. package/dist/room/defaults.d.ts +0 -5
  90. package/dist/room/defaults.js +0 -32
  91. package/dist/room/defaults.js.map +0 -1
  92. package/dist/room/track/create.d.ts +0 -25
  93. package/dist/room/track/create.js +0 -170
  94. package/dist/room/track/create.js.map +0 -1
package/example/sample.ts CHANGED
@@ -1,82 +1,244 @@
1
1
  import {
2
- connect, CreateVideoTrackOptions, DataPacket_Kind, LocalTrack, LocalTrackPublication, LogLevel,
2
+ DataPacket_Kind, LocalParticipant,
3
3
  MediaDeviceFailure,
4
4
  Participant,
5
5
  ParticipantEvent,
6
- RemoteParticipant,
7
- RemoteTrack,
8
- RemoteTrackPublication,
9
- Room,
10
- RoomEvent,
11
- Track, TrackPublication, VideoPresets,
6
+ RemoteParticipant, Room, RoomConnectOptions, RoomEvent,
7
+ RoomOptions, RoomState, setLogLevel, Track, TrackPublication,
8
+ VideoCaptureOptions, VideoPresets,
12
9
  } from '../src/index';
13
10
  import { ConnectionQuality } from '../src/room/participant/Participant';
14
11
 
15
12
  const $ = (id: string) => document.getElementById(id);
16
13
 
17
- declare global {
18
- interface Window {
19
- connectWithFormInput: any;
20
- connectToRoom: any;
21
- handleDeviceSelected: any;
22
- shareScreen: any;
23
- toggleVideo: any;
24
- toggleAudio: any;
25
- enterText: any;
26
- disconnectSignal: any;
27
- disconnectRoom: any;
28
- currentRoom: any;
29
- startAudio: any;
30
- flipVideo: any;
31
- }
32
- }
14
+ const state = {
15
+ isFrontFacing: false,
16
+ encoder: new TextEncoder(),
17
+ decoder: new TextDecoder(),
18
+ defaultDevices: new Map<MediaDeviceKind, string>(),
19
+ };
33
20
 
34
- function appendLog(...args: any[]) {
35
- const logger = $('log')!;
36
- for (let i = 0; i < arguments.length; i += 1) {
37
- if (typeof args[i] === 'object') {
38
- logger.innerHTML
39
- += `${JSON && JSON.stringify
40
- ? JSON.stringify(args[i], undefined, 2)
41
- : args[i]} `;
21
+ let currentRoom: Room | undefined;
22
+
23
+ // handles actions from the HTML
24
+ const appActions = {
25
+ connectWithFormInput: async () => {
26
+ const url = (<HTMLInputElement>$('url')).value;
27
+ const token = (<HTMLInputElement>$('token')).value;
28
+ const simulcast = (<HTMLInputElement>$('simulcast')).checked;
29
+ const forceTURN = (<HTMLInputElement>$('force-turn')).checked;
30
+ const adaptiveVideo = (<HTMLInputElement>$('adaptive-video')).checked;
31
+ const shouldPublish = (<HTMLInputElement>$('publish-option')).checked;
32
+
33
+ setLogLevel('debug');
34
+
35
+ const roomOpts: RoomOptions = {
36
+ autoManageVideo: adaptiveVideo,
37
+ publishDefaults: {
38
+ simulcast,
39
+ },
40
+ videoCaptureDefaults: {
41
+ resolution: VideoPresets.hd.resolution,
42
+ },
43
+ };
44
+
45
+ const connectOpts: RoomConnectOptions = {};
46
+ if (forceTURN) {
47
+ connectOpts.rtcConfig = {
48
+ iceTransportPolicy: 'relay',
49
+ };
50
+ }
51
+ const room = await appActions.connectToRoom(url, token, roomOpts, connectOpts);
52
+
53
+ if (room && shouldPublish) {
54
+ await room.localParticipant.enableCameraAndMicrophone();
55
+ updateButtonsForPublishState();
56
+ }
57
+ },
58
+
59
+ connectToRoom: async (
60
+ url: string,
61
+ token: string,
62
+ roomOptions?: RoomOptions,
63
+ connectOptions?: RoomConnectOptions,
64
+ ): Promise<Room | undefined> => {
65
+ const room = new Room(roomOptions);
66
+ room
67
+ .on(RoomEvent.ParticipantConnected, participantConnected)
68
+ .on(RoomEvent.ParticipantDisconnected, participantDisconnected)
69
+ .on(RoomEvent.DataReceived, handleData)
70
+ .on(RoomEvent.Disconnected, handleRoomDisconnect)
71
+ .on(RoomEvent.Reconnecting, () => appendLog('Reconnecting to room'))
72
+ .on(RoomEvent.Reconnected, () => appendLog('Successfully reconnected!'))
73
+ .on(RoomEvent.LocalTrackPublished, () => {
74
+ renderParticipant(room.localParticipant);
75
+ updateButtonsForPublishState();
76
+ renderScreenShare();
77
+ })
78
+ .on(RoomEvent.LocalTrackUnpublished, () => {
79
+ renderParticipant(room.localParticipant);
80
+ updateButtonsForPublishState();
81
+ renderScreenShare();
82
+ })
83
+ .on(RoomEvent.RoomMetadataChanged, (metadata) => {
84
+ appendLog('new metadata for room', metadata);
85
+ })
86
+ .on(RoomEvent.MediaDevicesChanged, handleDevicesChanged)
87
+ .on(RoomEvent.AudioPlaybackStatusChanged, () => {
88
+ if (room.canPlaybackAudio) {
89
+ $('start-audio-button')?.setAttribute('disabled', 'true');
90
+ } else {
91
+ $('start-audio-button')?.removeAttribute('disabled');
92
+ }
93
+ })
94
+ .on(RoomEvent.MediaDevicesError, (e: Error) => {
95
+ const failure = MediaDeviceFailure.getFailure(e);
96
+ appendLog('media device failure', failure);
97
+ })
98
+ .on(RoomEvent.ConnectionQualityChanged,
99
+ (quality: ConnectionQuality, participant: Participant) => {
100
+ appendLog('connection quality changed', participant.identity, quality);
101
+ });
102
+
103
+ try {
104
+ await room.connect(url, token, connectOptions);
105
+ } catch (error) {
106
+ let message: any = error;
107
+ if (error.message) {
108
+ message = error.message;
109
+ }
110
+ appendLog('could not connect:', message);
111
+ return;
112
+ }
113
+
114
+ appendLog('connected to room', room.name);
115
+ currentRoom = room;
116
+ window.currentRoom = room;
117
+ setButtonsForState(true);
118
+
119
+ appendLog('room participants', room.participants.keys());
120
+ room.participants.forEach((participant) => {
121
+ participantConnected(participant);
122
+ });
123
+ participantConnected(room.localParticipant);
124
+
125
+ return room;
126
+ },
127
+
128
+ toggleAudio: async () => {
129
+ if (!currentRoom) return;
130
+ const enabled = currentRoom.localParticipant.isMicrophoneEnabled;
131
+ if (enabled) {
132
+ appendLog('disabling audio');
42
133
  } else {
43
- logger.innerHTML += `${args[i]} `;
134
+ appendLog('enabling audio');
44
135
  }
45
- }
46
- logger.innerHTML += '\n';
47
- (() => {
48
- logger.scrollTop = logger.scrollHeight;
49
- })();
50
- }
136
+ await currentRoom.localParticipant.setMicrophoneEnabled(!enabled);
137
+ updateButtonsForPublishState();
138
+ },
139
+
140
+ toggleVideo: async () => {
141
+ if (!currentRoom) return;
142
+ const enabled = currentRoom.localParticipant.isCameraEnabled;
143
+ if (enabled) {
144
+ appendLog('disabling video');
145
+ } else {
146
+ appendLog('enabling video');
147
+ }
148
+ await currentRoom.localParticipant.setCameraEnabled(!enabled);
149
+ renderParticipant(currentRoom.localParticipant);
51
150
 
52
- function trackSubscribed(
53
- div: HTMLDivElement,
54
- track: Track,
55
- participant: Participant,
56
- ): HTMLMediaElement | null {
57
- appendLog('track subscribed', track);
58
- const element = track.attach();
59
- div.appendChild(element);
60
- return element;
61
- }
151
+ // update display
152
+ updateButtonsForPublishState();
153
+ },
62
154
 
63
- function trackUnsubscribed(
64
- track: RemoteTrack | LocalTrack,
65
- pub?: RemoteTrackPublication,
66
- participant?: Participant,
67
- ) {
68
- let logName = track.name;
69
- if (track.sid) {
70
- logName = track.sid;
155
+ flipVideo: () => {
156
+ const videoPub = currentRoom?.localParticipant.getTrack(Track.Source.Camera);
157
+ if (!videoPub) {
158
+ return;
159
+ }
160
+ if (state.isFrontFacing) {
161
+ setButtonState('flip-video-button', 'Front Camera', false);
162
+ } else {
163
+ setButtonState('flip-video-button', 'Back Camera', false);
164
+ }
165
+ state.isFrontFacing = !state.isFrontFacing;
166
+ const options: VideoCaptureOptions = {
167
+ resolution: VideoPresets.qhd.resolution,
168
+ facingMode: state.isFrontFacing ? 'user' : 'environment',
169
+ };
170
+ videoPub.videoTrack?.restartTrack(options);
171
+ },
172
+
173
+ shareScreen: async () => {
174
+ if (!currentRoom) return;
175
+
176
+ const enabled = currentRoom.localParticipant.isScreenShareEnabled;
177
+ appendLog(`${enabled ? 'stopping' : 'starting'} screen share`);
178
+ await currentRoom.localParticipant.setScreenShareEnabled(!enabled);
179
+ updateButtonsForPublishState();
180
+ },
181
+
182
+ startAudio: () => {
183
+ currentRoom?.startAudio();
184
+ },
185
+
186
+ enterText: () => {
187
+ if (!currentRoom) return;
188
+ const textField = <HTMLInputElement>$('entry');
189
+ if (textField.value) {
190
+ const msg = state.encoder.encode(textField.value);
191
+ currentRoom.localParticipant.publishData(msg, DataPacket_Kind.RELIABLE);
192
+ (<HTMLTextAreaElement>(
193
+ $('chat')
194
+ )).value += `${currentRoom.localParticipant.identity} (me): ${textField.value}\n`;
195
+ textField.value = '';
196
+ }
197
+ },
198
+
199
+ disconnectRoom: () => {
200
+ if (currentRoom) {
201
+ currentRoom.disconnect();
202
+ }
203
+ },
204
+
205
+ disconnectSignal: () => {
206
+ if (!currentRoom) return;
207
+ currentRoom.engine.client.close();
208
+ if (currentRoom.engine.client.onClose) {
209
+ currentRoom.engine.client.onClose('manual disconnect');
210
+ }
211
+ },
212
+
213
+ handleDeviceSelected: async (e: Event) => {
214
+ const deviceId = (<HTMLSelectElement>e.target).value;
215
+ const elementId = (<HTMLSelectElement>e.target).id;
216
+ const kind = elementMapping[elementId];
217
+ if (!kind) {
218
+ return;
219
+ }
220
+
221
+ state.defaultDevices.set(kind, deviceId);
222
+
223
+ if (currentRoom) {
224
+ await currentRoom.switchActiveDevice(kind, deviceId);
225
+ }
226
+ },
227
+ };
228
+
229
+ declare global {
230
+ interface Window {
231
+ currentRoom: any;
232
+ appActions: typeof appActions;
71
233
  }
72
- appendLog('track unsubscribed', logName);
73
- track.detach().forEach((element) => element.remove());
74
234
  }
75
235
 
76
- const encoder = new TextEncoder();
77
- const decoder = new TextDecoder();
236
+ window.appActions = appActions;
237
+
238
+ // --------------------------- event handlers ------------------------------- //
239
+
78
240
  function handleData(msg: Uint8Array, participant?: RemoteParticipant) {
79
- const str = decoder.decode(msg);
241
+ const str = state.decoder.decode(msg);
80
242
  const chat = <HTMLTextAreaElement>$('chat');
81
243
  let from = 'server';
82
244
  if (participant) {
@@ -85,315 +247,240 @@ function handleData(msg: Uint8Array, participant?: RemoteParticipant) {
85
247
  chat.value += `${from}: ${str}\n`;
86
248
  }
87
249
 
88
- function handleSpeakerChanged(speakers: Participant[]) {
89
- // remove tags from all
90
- currentRoom.participants.forEach((participant) => {
91
- setParticipantSpeaking(participant, speakers.includes(participant));
92
- });
93
-
94
- // do the same for local participant
95
- setParticipantSpeaking(
96
- currentRoom.localParticipant,
97
- speakers.includes(currentRoom.localParticipant),
98
- );
99
- }
100
-
101
- function setParticipantSpeaking(participant: Participant, speaking: boolean) {
102
- participant.videoTracks.forEach((publication) => {
103
- const { track } = publication;
104
- if (track && track.kind === Track.Kind.Video) {
105
- track.attachedElements.forEach((element) => {
106
- if (speaking) {
107
- element.classList.add('speaking');
108
- } else {
109
- element.classList.remove('speaking');
110
- }
111
- });
112
- }
113
- });
114
- }
115
-
116
- function participantConnected(participant: RemoteParticipant) {
117
- appendLog('participant', participant.sid, 'connected', participant.metadata);
118
-
119
- const div = document.createElement('div');
120
- div.id = participant.sid;
121
- div.innerText = participant.identity;
122
- div.className = 'col-md-6 video-container';
123
- $('remote-area')?.appendChild(div);
124
-
125
- participant.on(ParticipantEvent.TrackSubscribed, (track) => {
126
- trackSubscribed(div, track, participant);
127
- });
128
- participant.on(ParticipantEvent.TrackUnsubscribed, (track, pub) => {
129
- trackUnsubscribed(track, pub, participant);
130
- });
250
+ function participantConnected(participant: Participant) {
251
+ appendLog('participant', participant.identity, 'connected', participant.metadata);
252
+ participant
253
+ .on(ParticipantEvent.TrackSubscribed, (_, pub: TrackPublication) => {
254
+ appendLog('subscribed to track', pub.trackSid, participant.identity);
255
+ renderParticipant(participant);
256
+ renderScreenShare();
257
+ })
258
+ .on(ParticipantEvent.TrackUnsubscribed, (_, pub: TrackPublication) => {
259
+ appendLog('unsubscribed from track', pub.trackSid);
260
+ renderParticipant(participant);
261
+ renderScreenShare();
262
+ })
263
+ .on(ParticipantEvent.TrackMuted, (pub: TrackPublication) => {
264
+ appendLog('track was muted', pub.trackSid, participant.identity);
265
+ renderParticipant(participant);
266
+ })
267
+ .on(ParticipantEvent.TrackUnmuted, (pub: TrackPublication) => {
268
+ appendLog('track was unmuted', pub.trackSid, participant.identity);
269
+ renderParticipant(participant);
270
+ })
271
+ .on(ParticipantEvent.IsSpeakingChanged, () => {
272
+ renderParticipant(participant);
273
+ })
274
+ .on(ParticipantEvent.ConnectionQualityChanged, () => {
275
+ renderParticipant(participant);
276
+ });
131
277
  }
132
278
 
133
279
  function participantDisconnected(participant: RemoteParticipant) {
134
280
  appendLog('participant', participant.sid, 'disconnected');
135
281
 
136
- $(participant.sid)?.remove();
282
+ renderParticipant(participant, true);
137
283
  }
138
284
 
139
285
  function handleRoomDisconnect() {
286
+ if (!currentRoom) return;
140
287
  appendLog('disconnected from room');
141
288
  setButtonsForState(false);
142
- $('local-video')!.innerHTML = '';
143
-
144
- // clear the chat area on disconnect
145
- clearChat();
146
-
147
- // clear remote area on disconnect
148
- clearRemoteArea();
149
- }
150
-
151
- function setButtonState(buttonId: string, buttonText: string, isActive: boolean) {
152
- const el = $(buttonId);
153
- if (!el) return;
289
+ renderParticipant(currentRoom.localParticipant, true);
290
+ currentRoom.participants.forEach((p) => {
291
+ renderParticipant(p, true);
292
+ });
293
+ renderScreenShare();
154
294
 
155
- el.innerHTML = buttonText;
156
- if (isActive) {
157
- el.classList.add('active');
158
- } else {
159
- el.classList.remove('active');
295
+ const container = $('participants-area');
296
+ if (container) {
297
+ container.innerHTML = '';
160
298
  }
161
- }
162
299
 
163
- function clearChat() {
300
+ // clear the chat area on disconnect
164
301
  const chat = <HTMLTextAreaElement>$('chat');
165
302
  chat.value = '';
303
+
304
+ currentRoom = undefined;
305
+ window.currentRoom = undefined;
166
306
  }
167
307
 
168
- function clearRemoteArea() {
169
- const el = $('remote-area');
170
- if (!el) return;
308
+ // -------------------------- rendering helpers ----------------------------- //
171
309
 
172
- while (el.firstChild) {
173
- el.removeChild(el.firstChild);
310
+ function appendLog(...args: any[]) {
311
+ const logger = $('log')!;
312
+ for (let i = 0; i < arguments.length; i += 1) {
313
+ if (typeof args[i] === 'object') {
314
+ logger.innerHTML
315
+ += `${JSON && JSON.stringify
316
+ ? JSON.stringify(args[i], undefined, 2)
317
+ : args[i]} `;
318
+ } else {
319
+ logger.innerHTML += `${args[i]} `;
320
+ }
174
321
  }
322
+ logger.innerHTML += '\n';
323
+ (() => {
324
+ logger.scrollTop = logger.scrollHeight;
325
+ })();
175
326
  }
176
327
 
177
- let currentRoom: Room;
178
- window.connectWithFormInput = () => {
179
- const url = (<HTMLInputElement>$('url')).value;
180
- const token = (<HTMLInputElement>$('token')).value;
181
- const simulcast = (<HTMLInputElement>$('simulcast')).checked;
182
- const forceTURN = (<HTMLInputElement>$('force-turn')).checked;
183
-
184
- window.connectToRoom(url, token, simulcast, forceTURN);
185
- };
186
-
187
- window.connectToRoom = async (
188
- url: string,
189
- token: string,
190
- simulcast: boolean = false,
191
- forceTURN: boolean = false,
192
- ) => {
193
- let room: Room;
194
- const rtcConfig: RTCConfiguration = {};
195
- if (forceTURN) {
196
- rtcConfig.iceTransportPolicy = 'relay';
328
+ // updates participant UI
329
+ function renderParticipant(participant: Participant, remove: boolean = false) {
330
+ const container = $('participants-area');
331
+ if (!container) return;
332
+ let div = $(`participant-${participant.sid}`);
333
+ if (!div && !remove) {
334
+ div = document.createElement('div');
335
+ div.id = `participant-${participant.sid}`;
336
+ div.className = 'participant';
337
+ div.innerHTML = `
338
+ <video id="video-${participant.sid}"></video>
339
+ <audio id="audio-${participant.sid}"></audio>
340
+ <div class="info-bar">
341
+ <div id="name-${participant.sid}" class="name">
342
+ </div>
343
+ <div id="size-${participant.sid}" class="size">
344
+ </div>
345
+ <div class="right">
346
+ <span id="signal-${participant.sid}"></span>
347
+ <span id="mic-${participant.sid}" class="mic-on"></span>
348
+ </div>
349
+ </div>
350
+ `;
351
+ container.appendChild(div);
352
+
353
+ const sizeElm = $(`size-${participant.sid}`);
354
+ const videoElm = <HTMLVideoElement>$(`video-${participant.sid}`);
355
+ videoElm.onresize = () => {
356
+ updateVideoSize(videoElm!, sizeElm!);
357
+ };
197
358
  }
198
- const shouldPublish = (<HTMLInputElement>$('publish-option')).checked;
199
-
200
- try {
201
- room = await connect(url, token, {
202
- logLevel: LogLevel.debug,
203
- rtcConfig,
204
- audio: shouldPublish,
205
- video: shouldPublish,
206
- publishDefaults: {
207
- simulcast,
208
- },
209
- });
210
- } catch (error) {
211
- let message: any = error;
212
- if (error.message) {
213
- message = error.message;
359
+ const videoElm = <HTMLVideoElement>$(`video-${participant.sid}`);
360
+ const audioELm = <HTMLAudioElement>$(`audio-${participant.sid}`);
361
+ if (remove) {
362
+ div?.remove();
363
+ if (videoElm) {
364
+ videoElm.srcObject = null;
365
+ videoElm.src = '';
366
+ }
367
+ if (audioELm) {
368
+ audioELm.srcObject = null;
369
+ audioELm.src = '';
214
370
  }
215
- appendLog('could not connect:', message);
216
371
  return;
217
372
  }
218
373
 
219
- appendLog('connected to room', room.name);
220
- currentRoom = room;
221
- window.currentRoom = room;
222
- setButtonsForState(true);
223
- updateButtonsForPublishState();
224
-
225
- room
226
- .on(RoomEvent.ParticipantConnected, participantConnected)
227
- .on(RoomEvent.ParticipantDisconnected, participantDisconnected)
228
- .on(RoomEvent.DataReceived, handleData)
229
- .on(RoomEvent.ActiveSpeakersChanged, handleSpeakerChanged)
230
- .on(RoomEvent.Disconnected, handleRoomDisconnect)
231
- .on(RoomEvent.Reconnecting, () => appendLog('Reconnecting to room'))
232
- .on(RoomEvent.Reconnected, () => appendLog('Successfully reconnected!'))
233
- .on(RoomEvent.TrackMuted, (pub: TrackPublication, p: Participant) => appendLog('track was muted', pub.trackSid, p.identity))
234
- .on(RoomEvent.TrackUnmuted, (pub: TrackPublication, p: Participant) => appendLog('track was unmuted', pub.trackSid, p.identity))
235
- .on(RoomEvent.LocalTrackPublished, (pub: LocalTrackPublication) => {
236
- if (pub.kind === Track.Kind.Video) {
237
- attachLocalVideo();
238
- }
239
- updateButtonsForPublishState();
240
- })
241
- .on(RoomEvent.RoomMetadataChanged, (metadata) => {
242
- appendLog('new metadata for room', metadata);
243
- })
244
- .on(RoomEvent.MediaDevicesChanged, handleDevicesChanged)
245
- .on(RoomEvent.AudioPlaybackStatusChanged, () => {
246
- if (room.canPlaybackAudio) {
247
- $('start-audio-button')?.setAttribute('disabled', 'true');
248
- } else {
249
- $('start-audio-button')?.removeAttribute('disabled');
250
- }
251
- })
252
- .on(RoomEvent.MediaDevicesError, (e: Error) => {
253
- const failure = MediaDeviceFailure.getFailure(e);
254
- appendLog('media device failure', failure);
255
- })
256
- .on(RoomEvent.ConnectionQualityChanged,
257
- (quality: ConnectionQuality, participant: Participant) => {
258
- appendLog('connection quality changed', participant.identity, quality);
259
- });
260
-
261
- appendLog('room participants', room.participants.keys());
262
- room.participants.forEach((participant) => {
263
- participantConnected(participant);
264
- });
265
-
266
- $('local-video')!.innerHTML = `${room.localParticipant.identity} (me)`;
267
- };
268
-
269
- window.toggleVideo = async () => {
270
- if (!currentRoom) return;
271
- const video = getMyVideo();
272
- if (currentRoom.localParticipant.isCameraEnabled) {
273
- appendLog('disabling video');
274
- await currentRoom.localParticipant.setCameraEnabled(false);
275
- // hide from display
276
- if (video) {
277
- video.style.display = 'none';
278
- }
374
+ // update properties
375
+ $(`name-${participant.sid}`)!.innerHTML = participant.identity;
376
+ const micElm = $(`mic-${participant.sid}`)!;
377
+ const signalElm = $(`signal-${participant.sid}`)!;
378
+ const cameraPub = participant.getTrack(Track.Source.Camera);
379
+ const micPub = participant.getTrack(Track.Source.Microphone);
380
+ if (participant.isSpeaking) {
381
+ div!.classList.add('speaking');
279
382
  } else {
280
- appendLog('enabling video');
281
- await currentRoom.localParticipant.setCameraEnabled(true);
282
- attachLocalVideo();
283
- if (video) {
284
- video.style.display = '';
285
- }
383
+ div!.classList.remove('speaking');
286
384
  }
287
- updateButtonsForPublishState();
288
- };
289
385
 
290
- window.toggleAudio = async () => {
291
- if (!currentRoom) return;
292
- if (currentRoom.localParticipant.isMicrophoneEnabled) {
293
- appendLog('disabling audio');
294
- await currentRoom.localParticipant.setMicrophoneEnabled(false);
386
+ const cameraEnabled = cameraPub && cameraPub.isSubscribed && !cameraPub.isMuted;
387
+ if (cameraEnabled) {
388
+ cameraPub?.videoTrack?.attach(videoElm);
389
+ if (participant instanceof LocalParticipant) {
390
+ // flip
391
+ videoElm.style.transform = 'scale(-1, 1)';
392
+ }
393
+ } else if (cameraPub?.videoTrack) {
394
+ // detach manually whenever possible
395
+ cameraPub.videoTrack?.detach(videoElm);
295
396
  } else {
296
- appendLog('enabling audio');
297
- await currentRoom.localParticipant.setMicrophoneEnabled(true);
397
+ videoElm.src = '';
398
+ videoElm.srcObject = null;
298
399
  }
299
- updateButtonsForPublishState();
300
- };
301
400
 
302
- window.enterText = () => {
303
- const textField = <HTMLInputElement>$('entry');
304
- if (textField.value) {
305
- const msg = encoder.encode(textField.value);
306
- currentRoom.localParticipant.publishData(msg, DataPacket_Kind.RELIABLE);
307
- (<HTMLTextAreaElement>(
308
- $('chat')
309
- )).value += `${currentRoom.localParticipant.identity} (me): ${textField.value}\n`;
310
- textField.value = '';
311
- }
312
- };
313
-
314
- window.shareScreen = async () => {
315
- if (!currentRoom) return;
316
-
317
- if (currentRoom.localParticipant.isScreenShareEnabled) {
318
- appendLog('stopping screen share');
319
- await currentRoom.localParticipant.setScreenShareEnabled(false);
401
+ const micEnabled = micPub && micPub.isSubscribed && !micPub.isMuted;
402
+ if (micEnabled) {
403
+ if (!(participant instanceof LocalParticipant)) {
404
+ // don't attach local audio
405
+ micPub?.audioTrack?.attach(audioELm);
406
+ }
407
+ micElm.className = 'mic-on';
408
+ micElm.innerHTML = '<i class="fas fa-microphone"></i>';
320
409
  } else {
321
- appendLog('starting screen share');
322
- await currentRoom.localParticipant.setScreenShareEnabled(true);
323
- appendLog('started screen share');
410
+ micElm.className = 'mic-off';
411
+ micElm.innerHTML = '<i class="fas fa-microphone-slash"></i>';
324
412
  }
325
- updateButtonsForPublishState();
326
- };
327
413
 
328
- window.disconnectSignal = () => {
329
- if (!currentRoom) return;
330
- currentRoom.engine.client.close();
331
- if (currentRoom.engine.client.onClose) {
332
- currentRoom.engine.client.onClose('manual disconnect');
333
- }
334
- };
335
-
336
- window.disconnectRoom = () => {
337
- if (currentRoom) {
338
- currentRoom.disconnect();
414
+ switch (participant.connectionQuality) {
415
+ case ConnectionQuality.Excellent:
416
+ case ConnectionQuality.Good:
417
+ case ConnectionQuality.Poor:
418
+ signalElm.className = `connection-${participant.connectionQuality}`;
419
+ signalElm.innerHTML = '<i class="fas fa-circle"></i>';
420
+ break;
421
+ default:
422
+ signalElm.innerHTML = '';
423
+ // do nothing
339
424
  }
340
- };
341
-
342
- window.startAudio = () => {
343
- currentRoom.startAudio();
344
- };
425
+ }
345
426
 
346
- let isFrontFacing = true;
347
- window.flipVideo = () => {
348
- const videoPub = currentRoom.localParticipant.getTrack(Track.Source.Camera);
349
- if (!videoPub) {
427
+ function renderScreenShare() {
428
+ const div = $('screenshare-area')!;
429
+ if (!currentRoom || currentRoom.state !== RoomState.Connected) {
430
+ div.style.display = 'none';
350
431
  return;
351
432
  }
352
- if (isFrontFacing) {
353
- setButtonState('flip-video-button', 'Front Camera', false);
433
+ let participant: Participant | undefined;
434
+ let screenSharePub: TrackPublication | undefined = currentRoom.localParticipant.getTrack(
435
+ Track.Source.ScreenShare,
436
+ );
437
+ if (!screenSharePub) {
438
+ currentRoom.participants.forEach((p) => {
439
+ if (screenSharePub) {
440
+ return;
441
+ }
442
+ participant = p;
443
+ const pub = p.getTrack(Track.Source.ScreenShare);
444
+ if (pub?.isSubscribed) {
445
+ screenSharePub = pub;
446
+ }
447
+ });
354
448
  } else {
355
- setButtonState('flip-video-button', 'Back Camera', false);
449
+ participant = currentRoom.localParticipant;
356
450
  }
357
- isFrontFacing = !isFrontFacing;
358
- const options: CreateVideoTrackOptions = {
359
- resolution: VideoPresets.qhd.resolution,
360
- facingMode: isFrontFacing ? 'user' : 'environment',
361
- };
362
- videoPub.videoTrack?.restartTrack(options);
363
- };
364
-
365
- const defaultDevices = new Map<MediaDeviceKind, string>();
366
- window.handleDeviceSelected = async (e: Event) => {
367
- const deviceId = (<HTMLSelectElement>e.target).value;
368
- const elementId = (<HTMLSelectElement>e.target).id;
369
- const kind = elementMapping[elementId];
370
- if (!kind) {
371
- return;
372
- }
373
-
374
- defaultDevices.set(kind, deviceId);
375
451
 
376
- if (currentRoom) {
377
- await currentRoom.switchActiveDevice(kind, deviceId);
452
+ if (screenSharePub && participant) {
453
+ div.style.display = 'block';
454
+ const videoElm = <HTMLVideoElement>$('screenshare-video');
455
+ screenSharePub.videoTrack?.attach(videoElm);
456
+ videoElm.onresize = () => {
457
+ updateVideoSize(videoElm, <HTMLSpanElement>$('screenshare-resolution'));
458
+ };
459
+ const infoElm = $('screenshare-info')!;
460
+ infoElm.innerHTML = `Screenshare from ${participant.identity}`;
461
+ } else {
462
+ div.style.display = 'none';
378
463
  }
379
- };
464
+ }
380
465
 
381
- setTimeout(handleDevicesChanged, 100);
466
+ function updateVideoSize(element: HTMLVideoElement, target: HTMLElement) {
467
+ target.innerHTML = `(${element.videoWidth}x${element.videoHeight})`;
468
+ }
382
469
 
383
- async function attachLocalVideo() {
384
- const videoPub = currentRoom.localParticipant.getTrack(Track.Source.Camera);
385
- const videoTrack = videoPub?.videoTrack;
386
- if (!videoTrack) {
387
- return;
388
- }
470
+ function setButtonState(buttonId: string, buttonText: string, isActive: boolean) {
471
+ const el = $(buttonId);
472
+ if (!el) return;
389
473
 
390
- if (videoTrack.attachedElements.length === 0) {
391
- const video = videoTrack.attach();
392
- video.style.transform = 'scale(-1, 1)';
393
- $('local-video')!.appendChild(video);
474
+ el.innerHTML = buttonText;
475
+ if (isActive) {
476
+ el.classList.add('active');
477
+ } else {
478
+ el.classList.remove('active');
394
479
  }
395
480
  }
396
481
 
482
+ setTimeout(handleDevicesChanged, 100);
483
+
397
484
  function setButtonsForState(connected: boolean) {
398
485
  const connectedSet = [
399
486
  'toggle-video-button',
@@ -402,6 +489,7 @@ function setButtonsForState(connected: boolean) {
402
489
  'disconnect-ws-button',
403
490
  'disconnect-room-button',
404
491
  'flip-video-button',
492
+ 'send-button',
405
493
  ];
406
494
  const disconnectedSet = ['connect-button'];
407
495
 
@@ -412,10 +500,6 @@ function setButtonsForState(connected: boolean) {
412
500
  toAdd.forEach((id) => $(id)?.setAttribute('disabled', 'true'));
413
501
  }
414
502
 
415
- function getMyVideo() {
416
- return <HTMLVideoElement>document.querySelector('#local-video video');
417
- }
418
-
419
503
  const elementMapping: { [k: string]: MediaDeviceKind } = {
420
504
  'video-input': 'videoinput',
421
505
  'audio-input': 'audioinput',
@@ -429,7 +513,7 @@ async function handleDevicesChanged() {
429
513
  }
430
514
  const devices = await Room.getLocalDevices(kind);
431
515
  const element = <HTMLSelectElement>$(id);
432
- populateSelect(kind, element, devices, defaultDevices.get(kind));
516
+ populateSelect(kind, element, devices, state.defaultDevices.get(kind));
433
517
  }));
434
518
  }
435
519