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