livekit-client 0.14.3 → 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 (90) 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 +68 -73
  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 +67 -12
  16. package/dist/options.js +0 -10
  17. package/dist/options.js.map +1 -1
  18. package/dist/proto/livekit_models.d.ts +27 -1
  19. package/dist/proto/livekit_models.js +188 -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 +9 -18
  28. package/dist/room/Room.js +61 -36
  29. package/dist/room/Room.js.map +1 -1
  30. package/dist/room/events.d.ts +93 -0
  31. package/dist/room/events.js +93 -0
  32. package/dist/room/events.js.map +1 -1
  33. package/dist/room/participant/LocalParticipant.d.ts +27 -11
  34. package/dist/room/participant/LocalParticipant.js +183 -155
  35. package/dist/room/participant/LocalParticipant.js.map +1 -1
  36. package/dist/room/participant/Participant.js +2 -1
  37. package/dist/room/participant/Participant.js.map +1 -1
  38. package/dist/room/participant/RemoteParticipant.js +3 -2
  39. package/dist/room/participant/RemoteParticipant.js.map +1 -1
  40. package/dist/room/participant/publishUtils.d.ts +11 -0
  41. package/dist/room/participant/publishUtils.js +148 -0
  42. package/dist/room/participant/publishUtils.js.map +1 -0
  43. package/dist/room/participant/publishUtils.test.d.ts +1 -0
  44. package/dist/room/participant/publishUtils.test.js +79 -0
  45. package/dist/room/participant/publishUtils.test.js.map +1 -0
  46. package/dist/room/track/LocalAudioTrack.d.ts +4 -3
  47. package/dist/room/track/LocalAudioTrack.js +5 -3
  48. package/dist/room/track/LocalAudioTrack.js.map +1 -1
  49. package/dist/room/track/LocalTrack.d.ts +1 -3
  50. package/dist/room/track/LocalTrack.js +2 -49
  51. package/dist/room/track/LocalTrack.js.map +1 -1
  52. package/dist/room/track/LocalVideoTrack.d.ts +6 -4
  53. package/dist/room/track/LocalVideoTrack.js +41 -12
  54. package/dist/room/track/LocalVideoTrack.js.map +1 -1
  55. package/dist/room/track/LocalVideoTrack.test.d.ts +1 -0
  56. package/dist/room/track/LocalVideoTrack.test.js +68 -0
  57. package/dist/room/track/LocalVideoTrack.test.js.map +1 -0
  58. package/dist/room/track/RemoteTrackPublication.d.ts +1 -2
  59. package/dist/room/track/RemoteTrackPublication.js +5 -4
  60. package/dist/room/track/RemoteTrackPublication.js.map +1 -1
  61. package/dist/room/track/RemoteVideoTrack.d.ts +0 -2
  62. package/dist/room/track/RemoteVideoTrack.js +11 -19
  63. package/dist/room/track/RemoteVideoTrack.js.map +1 -1
  64. package/dist/room/track/Track.d.ts +1 -2
  65. package/dist/room/track/Track.js +1 -2
  66. package/dist/room/track/Track.js.map +1 -1
  67. package/dist/room/track/options.d.ts +15 -65
  68. package/dist/room/track/options.js +14 -13
  69. package/dist/room/track/options.js.map +1 -1
  70. package/dist/room/track/utils.d.ts +3 -0
  71. package/dist/room/track/utils.js +68 -0
  72. package/dist/room/track/utils.js.map +1 -0
  73. package/dist/room/track/utils.test.d.ts +1 -0
  74. package/dist/room/track/utils.test.js +85 -0
  75. package/dist/room/track/utils.test.js.map +1 -0
  76. package/dist/version.d.ts +1 -1
  77. package/dist/version.js +1 -1
  78. package/example/index.html +183 -178
  79. package/example/sample.ts +410 -327
  80. package/example/styles.css +144 -0
  81. package/example/webpack.config.js +1 -1
  82. package/jest.config.js +6 -0
  83. package/package.json +8 -5
  84. package/tsconfig.eslint.json +8 -1
  85. package/dist/room/defaults.d.ts +0 -5
  86. package/dist/room/defaults.js +0 -32
  87. package/dist/room/defaults.js.map +0 -1
  88. package/dist/room/track/create.d.ts +0 -25
  89. package/dist/room/track/create.js +0 -172
  90. 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,316 +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
- autoManageVideo: true,
207
- publishDefaults: {
208
- simulcast,
209
- },
210
- });
211
- } catch (error) {
212
- let message: any = error;
213
- if (error.message) {
214
- 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 = '';
215
370
  }
216
- appendLog('could not connect:', message);
217
371
  return;
218
372
  }
219
373
 
220
- appendLog('connected to room', room.name);
221
- currentRoom = room;
222
- window.currentRoom = room;
223
- setButtonsForState(true);
224
- updateButtonsForPublishState();
225
-
226
- room
227
- .on(RoomEvent.ParticipantConnected, participantConnected)
228
- .on(RoomEvent.ParticipantDisconnected, participantDisconnected)
229
- .on(RoomEvent.DataReceived, handleData)
230
- .on(RoomEvent.ActiveSpeakersChanged, handleSpeakerChanged)
231
- .on(RoomEvent.Disconnected, handleRoomDisconnect)
232
- .on(RoomEvent.Reconnecting, () => appendLog('Reconnecting to room'))
233
- .on(RoomEvent.Reconnected, () => appendLog('Successfully reconnected!'))
234
- .on(RoomEvent.TrackMuted, (pub: TrackPublication, p: Participant) => appendLog('track was muted', pub.trackSid, p.identity))
235
- .on(RoomEvent.TrackUnmuted, (pub: TrackPublication, p: Participant) => appendLog('track was unmuted', pub.trackSid, p.identity))
236
- .on(RoomEvent.LocalTrackPublished, (pub: LocalTrackPublication) => {
237
- if (pub.kind === Track.Kind.Video) {
238
- attachLocalVideo();
239
- }
240
- updateButtonsForPublishState();
241
- })
242
- .on(RoomEvent.RoomMetadataChanged, (metadata) => {
243
- appendLog('new metadata for room', metadata);
244
- })
245
- .on(RoomEvent.MediaDevicesChanged, handleDevicesChanged)
246
- .on(RoomEvent.AudioPlaybackStatusChanged, () => {
247
- if (room.canPlaybackAudio) {
248
- $('start-audio-button')?.setAttribute('disabled', 'true');
249
- } else {
250
- $('start-audio-button')?.removeAttribute('disabled');
251
- }
252
- })
253
- .on(RoomEvent.MediaDevicesError, (e: Error) => {
254
- const failure = MediaDeviceFailure.getFailure(e);
255
- appendLog('media device failure', failure);
256
- })
257
- .on(RoomEvent.ConnectionQualityChanged,
258
- (quality: ConnectionQuality, participant: Participant) => {
259
- appendLog('connection quality changed', participant.identity, quality);
260
- });
261
-
262
- appendLog('room participants', room.participants.keys());
263
- room.participants.forEach((participant) => {
264
- participantConnected(participant);
265
- });
266
-
267
- $('local-video')!.innerHTML = `${room.localParticipant.identity} (me)`;
268
- };
269
-
270
- window.toggleVideo = async () => {
271
- if (!currentRoom) return;
272
- const video = getMyVideo();
273
- if (currentRoom.localParticipant.isCameraEnabled) {
274
- appendLog('disabling video');
275
- await currentRoom.localParticipant.setCameraEnabled(false);
276
- // hide from display
277
- if (video) {
278
- video.style.display = 'none';
279
- }
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');
280
382
  } else {
281
- appendLog('enabling video');
282
- await currentRoom.localParticipant.setCameraEnabled(true);
283
- attachLocalVideo();
284
- if (video) {
285
- video.style.display = '';
286
- }
383
+ div!.classList.remove('speaking');
287
384
  }
288
- updateButtonsForPublishState();
289
- };
290
385
 
291
- window.toggleAudio = async () => {
292
- if (!currentRoom) return;
293
- if (currentRoom.localParticipant.isMicrophoneEnabled) {
294
- appendLog('disabling audio');
295
- 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);
296
396
  } else {
297
- appendLog('enabling audio');
298
- await currentRoom.localParticipant.setMicrophoneEnabled(true);
397
+ videoElm.src = '';
398
+ videoElm.srcObject = null;
299
399
  }
300
- updateButtonsForPublishState();
301
- };
302
400
 
303
- window.enterText = () => {
304
- const textField = <HTMLInputElement>$('entry');
305
- if (textField.value) {
306
- const msg = encoder.encode(textField.value);
307
- currentRoom.localParticipant.publishData(msg, DataPacket_Kind.RELIABLE);
308
- (<HTMLTextAreaElement>(
309
- $('chat')
310
- )).value += `${currentRoom.localParticipant.identity} (me): ${textField.value}\n`;
311
- textField.value = '';
312
- }
313
- };
314
-
315
- window.shareScreen = async () => {
316
- if (!currentRoom) return;
317
-
318
- if (currentRoom.localParticipant.isScreenShareEnabled) {
319
- appendLog('stopping screen share');
320
- 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>';
321
409
  } else {
322
- appendLog('starting screen share');
323
- await currentRoom.localParticipant.setScreenShareEnabled(true);
324
- appendLog('started screen share');
410
+ micElm.className = 'mic-off';
411
+ micElm.innerHTML = '<i class="fas fa-microphone-slash"></i>';
325
412
  }
326
- updateButtonsForPublishState();
327
- };
328
413
 
329
- window.disconnectSignal = () => {
330
- if (!currentRoom) return;
331
- currentRoom.engine.client.close();
332
- if (currentRoom.engine.client.onClose) {
333
- currentRoom.engine.client.onClose('manual disconnect');
334
- }
335
- };
336
-
337
- window.disconnectRoom = () => {
338
- if (currentRoom) {
339
- 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
340
424
  }
341
- };
342
-
343
- window.startAudio = () => {
344
- currentRoom.startAudio();
345
- };
425
+ }
346
426
 
347
- let isFrontFacing = true;
348
- window.flipVideo = () => {
349
- const videoPub = currentRoom.localParticipant.getTrack(Track.Source.Camera);
350
- if (!videoPub) {
427
+ function renderScreenShare() {
428
+ const div = $('screenshare-area')!;
429
+ if (!currentRoom || currentRoom.state !== RoomState.Connected) {
430
+ div.style.display = 'none';
351
431
  return;
352
432
  }
353
- if (isFrontFacing) {
354
- 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
+ });
355
448
  } else {
356
- setButtonState('flip-video-button', 'Back Camera', false);
449
+ participant = currentRoom.localParticipant;
357
450
  }
358
- isFrontFacing = !isFrontFacing;
359
- const options: CreateVideoTrackOptions = {
360
- resolution: VideoPresets.qhd.resolution,
361
- facingMode: isFrontFacing ? 'user' : 'environment',
362
- };
363
- videoPub.videoTrack?.restartTrack(options);
364
- };
365
-
366
- const defaultDevices = new Map<MediaDeviceKind, string>();
367
- window.handleDeviceSelected = async (e: Event) => {
368
- const deviceId = (<HTMLSelectElement>e.target).value;
369
- const elementId = (<HTMLSelectElement>e.target).id;
370
- const kind = elementMapping[elementId];
371
- if (!kind) {
372
- return;
373
- }
374
-
375
- defaultDevices.set(kind, deviceId);
376
451
 
377
- if (currentRoom) {
378
- 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';
379
463
  }
380
- };
464
+ }
381
465
 
382
- setTimeout(handleDevicesChanged, 100);
466
+ function updateVideoSize(element: HTMLVideoElement, target: HTMLElement) {
467
+ target.innerHTML = `(${element.videoWidth}x${element.videoHeight})`;
468
+ }
383
469
 
384
- async function attachLocalVideo() {
385
- const videoPub = currentRoom.localParticipant.getTrack(Track.Source.Camera);
386
- const videoTrack = videoPub?.videoTrack;
387
- if (!videoTrack) {
388
- return;
389
- }
470
+ function setButtonState(buttonId: string, buttonText: string, isActive: boolean) {
471
+ const el = $(buttonId);
472
+ if (!el) return;
390
473
 
391
- if (videoTrack.attachedElements.length === 0) {
392
- const video = videoTrack.attach();
393
- video.style.transform = 'scale(-1, 1)';
394
- $('local-video')!.appendChild(video);
474
+ el.innerHTML = buttonText;
475
+ if (isActive) {
476
+ el.classList.add('active');
477
+ } else {
478
+ el.classList.remove('active');
395
479
  }
396
480
  }
397
481
 
482
+ setTimeout(handleDevicesChanged, 100);
483
+
398
484
  function setButtonsForState(connected: boolean) {
399
485
  const connectedSet = [
400
486
  'toggle-video-button',
@@ -403,6 +489,7 @@ function setButtonsForState(connected: boolean) {
403
489
  'disconnect-ws-button',
404
490
  'disconnect-room-button',
405
491
  'flip-video-button',
492
+ 'send-button',
406
493
  ];
407
494
  const disconnectedSet = ['connect-button'];
408
495
 
@@ -413,10 +500,6 @@ function setButtonsForState(connected: boolean) {
413
500
  toAdd.forEach((id) => $(id)?.setAttribute('disabled', 'true'));
414
501
  }
415
502
 
416
- function getMyVideo() {
417
- return <HTMLVideoElement>document.querySelector('#local-video video');
418
- }
419
-
420
503
  const elementMapping: { [k: string]: MediaDeviceKind } = {
421
504
  'video-input': 'videoinput',
422
505
  'audio-input': 'audioinput',
@@ -430,7 +513,7 @@ async function handleDevicesChanged() {
430
513
  }
431
514
  const devices = await Room.getLocalDevices(kind);
432
515
  const element = <HTMLSelectElement>$(id);
433
- populateSelect(kind, element, devices, defaultDevices.get(kind));
516
+ populateSelect(kind, element, devices, state.defaultDevices.get(kind));
434
517
  }));
435
518
  }
436
519