livekit-client 0.14.3 → 0.15.3

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