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.
- package/.github/workflows/{lint.yaml → test.yaml} +7 -4
- package/README.md +27 -12
- package/dist/api/SignalClient.d.ts +4 -31
- package/dist/api/SignalClient.js +25 -7
- package/dist/api/SignalClient.js.map +1 -1
- package/dist/connect.d.ts +1 -1
- package/dist/connect.js +68 -73
- package/dist/connect.js.map +1 -1
- package/dist/index.d.ts +3 -4
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +10 -0
- package/dist/logger.js +14 -0
- package/dist/logger.js.map +1 -1
- package/dist/options.d.ts +67 -12
- package/dist/options.js +0 -10
- package/dist/options.js.map +1 -1
- package/dist/proto/livekit_models.d.ts +27 -1
- package/dist/proto/livekit_models.js +188 -3
- package/dist/proto/livekit_models.js.map +1 -1
- package/dist/proto/livekit_rtc.d.ts +49 -7
- package/dist/proto/livekit_rtc.js +303 -28
- package/dist/proto/livekit_rtc.js.map +1 -1
- package/dist/room/RTCEngine.d.ts +2 -2
- package/dist/room/RTCEngine.js +6 -4
- package/dist/room/RTCEngine.js.map +1 -1
- package/dist/room/Room.d.ts +9 -18
- package/dist/room/Room.js +61 -36
- package/dist/room/Room.js.map +1 -1
- package/dist/room/events.d.ts +93 -0
- package/dist/room/events.js +93 -0
- package/dist/room/events.js.map +1 -1
- package/dist/room/participant/LocalParticipant.d.ts +27 -11
- package/dist/room/participant/LocalParticipant.js +183 -155
- package/dist/room/participant/LocalParticipant.js.map +1 -1
- package/dist/room/participant/Participant.js +2 -1
- package/dist/room/participant/Participant.js.map +1 -1
- package/dist/room/participant/RemoteParticipant.js +3 -2
- 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/track/LocalAudioTrack.d.ts +4 -3
- package/dist/room/track/LocalAudioTrack.js +5 -3
- package/dist/room/track/LocalAudioTrack.js.map +1 -1
- package/dist/room/track/LocalTrack.d.ts +1 -3
- package/dist/room/track/LocalTrack.js +2 -49
- package/dist/room/track/LocalTrack.js.map +1 -1
- package/dist/room/track/LocalVideoTrack.d.ts +6 -4
- package/dist/room/track/LocalVideoTrack.js +41 -12
- 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/RemoteTrackPublication.d.ts +1 -2
- package/dist/room/track/RemoteTrackPublication.js +5 -4
- package/dist/room/track/RemoteTrackPublication.js.map +1 -1
- package/dist/room/track/RemoteVideoTrack.d.ts +0 -2
- package/dist/room/track/RemoteVideoTrack.js +11 -19
- package/dist/room/track/RemoteVideoTrack.js.map +1 -1
- package/dist/room/track/Track.d.ts +1 -2
- package/dist/room/track/Track.js +1 -2
- package/dist/room/track/Track.js.map +1 -1
- 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/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/example/index.html +183 -178
- package/example/sample.ts +410 -327
- package/example/styles.css +144 -0
- package/example/webpack.config.js +1 -1
- package/jest.config.js +6 -0
- package/package.json +8 -5
- 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/dist/room/track/create.d.ts +0 -25
- package/dist/room/track/create.js +0 -172
- package/dist/room/track/create.js.map +0 -1
package/example/sample.ts
CHANGED
@@ -1,82 +1,244 @@
|
|
1
1
|
import {
|
2
|
-
|
2
|
+
DataPacket_Kind, LocalParticipant,
|
3
3
|
MediaDeviceFailure,
|
4
4
|
Participant,
|
5
5
|
ParticipantEvent,
|
6
|
-
RemoteParticipant,
|
7
|
-
|
8
|
-
|
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
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
134
|
+
appendLog('enabling audio');
|
44
135
|
}
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
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
|
-
}
|
151
|
+
// update display
|
152
|
+
updateButtonsForPublishState();
|
153
|
+
},
|
62
154
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
-
|
77
|
-
|
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
|
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
|
-
});
|
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
|
-
|
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
|
-
|
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;
|
289
|
+
renderParticipant(currentRoom.localParticipant, true);
|
290
|
+
currentRoom.participants.forEach((p) => {
|
291
|
+
renderParticipant(p, true);
|
292
|
+
});
|
293
|
+
renderScreenShare();
|
154
294
|
|
155
|
-
|
156
|
-
if (
|
157
|
-
|
158
|
-
} else {
|
159
|
-
el.classList.remove('active');
|
295
|
+
const container = $('participants-area');
|
296
|
+
if (container) {
|
297
|
+
container.innerHTML = '';
|
160
298
|
}
|
161
|
-
}
|
162
299
|
|
163
|
-
|
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
|
-
|
169
|
-
const el = $('remote-area');
|
170
|
-
if (!el) return;
|
308
|
+
// -------------------------- rendering helpers ----------------------------- //
|
171
309
|
|
172
|
-
|
173
|
-
|
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
|
-
|
178
|
-
|
179
|
-
const
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
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
|
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;
|
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
|
-
|
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
|
-
}
|
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
|
-
|
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
|
-
|
292
|
-
if (
|
293
|
-
|
294
|
-
|
295
|
-
|
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
|
-
|
298
|
-
|
397
|
+
videoElm.src = '';
|
398
|
+
videoElm.srcObject = null;
|
299
399
|
}
|
300
|
-
updateButtonsForPublishState();
|
301
|
-
};
|
302
400
|
|
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);
|
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
|
-
|
323
|
-
|
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
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
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
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
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
|
-
|
354
|
-
|
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
|
-
|
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 (
|
378
|
-
|
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
|
-
|
466
|
+
function updateVideoSize(element: HTMLVideoElement, target: HTMLElement) {
|
467
|
+
target.innerHTML = `(${element.videoWidth}x${element.videoHeight})`;
|
468
|
+
}
|
383
469
|
|
384
|
-
|
385
|
-
const
|
386
|
-
|
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
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
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
|
|