livekit-client 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +70 -56
- 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 +77 -12
- package/dist/options.js +0 -10
- package/dist/options.js.map +1 -1
- package/dist/proto/livekit_models.d.ts +28 -1
- package/dist/proto/livekit_models.js +194 -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 +12 -14
- package/dist/room/Room.js +78 -45
- package/dist/room/Room.js.map +1 -1
- package/dist/room/events.d.ts +98 -1
- package/dist/room/events.js +97 -0
- package/dist/room/events.js.map +1 -1
- package/dist/room/participant/LocalParticipant.d.ts +27 -9
- package/dist/room/participant/LocalParticipant.js +194 -155
- package/dist/room/participant/LocalParticipant.js.map +1 -1
- package/dist/room/participant/Participant.js +6 -2
- 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/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 +10 -4
- package/dist/room/track/RemoteTrackPublication.js +60 -4
- package/dist/room/track/RemoteTrackPublication.js.map +1 -1
- package/dist/room/track/RemoteVideoTrack.d.ts +15 -1
- package/dist/room/track/RemoteVideoTrack.js +98 -1
- package/dist/room/track/RemoteVideoTrack.js.map +1 -1
- package/dist/room/track/Track.d.ts +15 -2
- package/dist/room/track/Track.js +6 -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/room/utils.d.ts +6 -0
- package/dist/room/utils.js +25 -1
- 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 +183 -178
- package/example/sample.ts +410 -326
- 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/dist/room/track/create.d.ts +0 -25
- package/dist/room/track/create.js +0 -170
- 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,315 +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
|
-
} catch (error) {
|
211
|
-
let message: any = error;
|
212
|
-
if (error.message) {
|
213
|
-
message = error.message;
|
359
|
+
const videoElm = <HTMLVideoElement>$(`video-${participant.sid}`);
|
360
|
+
const audioELm = <HTMLAudioElement>$(`audio-${participant.sid}`);
|
361
|
+
if (remove) {
|
362
|
+
div?.remove();
|
363
|
+
if (videoElm) {
|
364
|
+
videoElm.srcObject = null;
|
365
|
+
videoElm.src = '';
|
366
|
+
}
|
367
|
+
if (audioELm) {
|
368
|
+
audioELm.srcObject = null;
|
369
|
+
audioELm.src = '';
|
214
370
|
}
|
215
|
-
appendLog('could not connect:', message);
|
216
371
|
return;
|
217
372
|
}
|
218
373
|
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
.
|
227
|
-
.on(RoomEvent.ParticipantDisconnected, participantDisconnected)
|
228
|
-
.on(RoomEvent.DataReceived, handleData)
|
229
|
-
.on(RoomEvent.ActiveSpeakersChanged, handleSpeakerChanged)
|
230
|
-
.on(RoomEvent.Disconnected, handleRoomDisconnect)
|
231
|
-
.on(RoomEvent.Reconnecting, () => appendLog('Reconnecting to room'))
|
232
|
-
.on(RoomEvent.Reconnected, () => appendLog('Successfully reconnected!'))
|
233
|
-
.on(RoomEvent.TrackMuted, (pub: TrackPublication, p: Participant) => appendLog('track was muted', pub.trackSid, p.identity))
|
234
|
-
.on(RoomEvent.TrackUnmuted, (pub: TrackPublication, p: Participant) => appendLog('track was unmuted', pub.trackSid, p.identity))
|
235
|
-
.on(RoomEvent.LocalTrackPublished, (pub: LocalTrackPublication) => {
|
236
|
-
if (pub.kind === Track.Kind.Video) {
|
237
|
-
attachLocalVideo();
|
238
|
-
}
|
239
|
-
updateButtonsForPublishState();
|
240
|
-
})
|
241
|
-
.on(RoomEvent.RoomMetadataChanged, (metadata) => {
|
242
|
-
appendLog('new metadata for room', metadata);
|
243
|
-
})
|
244
|
-
.on(RoomEvent.MediaDevicesChanged, handleDevicesChanged)
|
245
|
-
.on(RoomEvent.AudioPlaybackStatusChanged, () => {
|
246
|
-
if (room.canPlaybackAudio) {
|
247
|
-
$('start-audio-button')?.setAttribute('disabled', 'true');
|
248
|
-
} else {
|
249
|
-
$('start-audio-button')?.removeAttribute('disabled');
|
250
|
-
}
|
251
|
-
})
|
252
|
-
.on(RoomEvent.MediaDevicesError, (e: Error) => {
|
253
|
-
const failure = MediaDeviceFailure.getFailure(e);
|
254
|
-
appendLog('media device failure', failure);
|
255
|
-
})
|
256
|
-
.on(RoomEvent.ConnectionQualityChanged,
|
257
|
-
(quality: ConnectionQuality, participant: Participant) => {
|
258
|
-
appendLog('connection quality changed', participant.identity, quality);
|
259
|
-
});
|
260
|
-
|
261
|
-
appendLog('room participants', room.participants.keys());
|
262
|
-
room.participants.forEach((participant) => {
|
263
|
-
participantConnected(participant);
|
264
|
-
});
|
265
|
-
|
266
|
-
$('local-video')!.innerHTML = `${room.localParticipant.identity} (me)`;
|
267
|
-
};
|
268
|
-
|
269
|
-
window.toggleVideo = async () => {
|
270
|
-
if (!currentRoom) return;
|
271
|
-
const video = getMyVideo();
|
272
|
-
if (currentRoom.localParticipant.isCameraEnabled) {
|
273
|
-
appendLog('disabling video');
|
274
|
-
await currentRoom.localParticipant.setCameraEnabled(false);
|
275
|
-
// hide from display
|
276
|
-
if (video) {
|
277
|
-
video.style.display = 'none';
|
278
|
-
}
|
374
|
+
// update properties
|
375
|
+
$(`name-${participant.sid}`)!.innerHTML = participant.identity;
|
376
|
+
const micElm = $(`mic-${participant.sid}`)!;
|
377
|
+
const signalElm = $(`signal-${participant.sid}`)!;
|
378
|
+
const cameraPub = participant.getTrack(Track.Source.Camera);
|
379
|
+
const micPub = participant.getTrack(Track.Source.Microphone);
|
380
|
+
if (participant.isSpeaking) {
|
381
|
+
div!.classList.add('speaking');
|
279
382
|
} else {
|
280
|
-
|
281
|
-
await currentRoom.localParticipant.setCameraEnabled(true);
|
282
|
-
attachLocalVideo();
|
283
|
-
if (video) {
|
284
|
-
video.style.display = '';
|
285
|
-
}
|
383
|
+
div!.classList.remove('speaking');
|
286
384
|
}
|
287
|
-
updateButtonsForPublishState();
|
288
|
-
};
|
289
385
|
|
290
|
-
|
291
|
-
if (
|
292
|
-
|
293
|
-
|
294
|
-
|
386
|
+
const cameraEnabled = cameraPub && cameraPub.isSubscribed && !cameraPub.isMuted;
|
387
|
+
if (cameraEnabled) {
|
388
|
+
cameraPub?.videoTrack?.attach(videoElm);
|
389
|
+
if (participant instanceof LocalParticipant) {
|
390
|
+
// flip
|
391
|
+
videoElm.style.transform = 'scale(-1, 1)';
|
392
|
+
}
|
393
|
+
} else if (cameraPub?.videoTrack) {
|
394
|
+
// detach manually whenever possible
|
395
|
+
cameraPub.videoTrack?.detach(videoElm);
|
295
396
|
} else {
|
296
|
-
|
297
|
-
|
397
|
+
videoElm.src = '';
|
398
|
+
videoElm.srcObject = null;
|
298
399
|
}
|
299
|
-
updateButtonsForPublishState();
|
300
|
-
};
|
301
400
|
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
textField.value = '';
|
311
|
-
}
|
312
|
-
};
|
313
|
-
|
314
|
-
window.shareScreen = async () => {
|
315
|
-
if (!currentRoom) return;
|
316
|
-
|
317
|
-
if (currentRoom.localParticipant.isScreenShareEnabled) {
|
318
|
-
appendLog('stopping screen share');
|
319
|
-
await currentRoom.localParticipant.setScreenShareEnabled(false);
|
401
|
+
const micEnabled = micPub && micPub.isSubscribed && !micPub.isMuted;
|
402
|
+
if (micEnabled) {
|
403
|
+
if (!(participant instanceof LocalParticipant)) {
|
404
|
+
// don't attach local audio
|
405
|
+
micPub?.audioTrack?.attach(audioELm);
|
406
|
+
}
|
407
|
+
micElm.className = 'mic-on';
|
408
|
+
micElm.innerHTML = '<i class="fas fa-microphone"></i>';
|
320
409
|
} else {
|
321
|
-
|
322
|
-
|
323
|
-
appendLog('started screen share');
|
410
|
+
micElm.className = 'mic-off';
|
411
|
+
micElm.innerHTML = '<i class="fas fa-microphone-slash"></i>';
|
324
412
|
}
|
325
|
-
updateButtonsForPublishState();
|
326
|
-
};
|
327
413
|
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
currentRoom.disconnect();
|
414
|
+
switch (participant.connectionQuality) {
|
415
|
+
case ConnectionQuality.Excellent:
|
416
|
+
case ConnectionQuality.Good:
|
417
|
+
case ConnectionQuality.Poor:
|
418
|
+
signalElm.className = `connection-${participant.connectionQuality}`;
|
419
|
+
signalElm.innerHTML = '<i class="fas fa-circle"></i>';
|
420
|
+
break;
|
421
|
+
default:
|
422
|
+
signalElm.innerHTML = '';
|
423
|
+
// do nothing
|
339
424
|
}
|
340
|
-
}
|
341
|
-
|
342
|
-
window.startAudio = () => {
|
343
|
-
currentRoom.startAudio();
|
344
|
-
};
|
425
|
+
}
|
345
426
|
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
427
|
+
function renderScreenShare() {
|
428
|
+
const div = $('screenshare-area')!;
|
429
|
+
if (!currentRoom || currentRoom.state !== RoomState.Connected) {
|
430
|
+
div.style.display = 'none';
|
350
431
|
return;
|
351
432
|
}
|
352
|
-
|
353
|
-
|
433
|
+
let participant: Participant | undefined;
|
434
|
+
let screenSharePub: TrackPublication | undefined = currentRoom.localParticipant.getTrack(
|
435
|
+
Track.Source.ScreenShare,
|
436
|
+
);
|
437
|
+
if (!screenSharePub) {
|
438
|
+
currentRoom.participants.forEach((p) => {
|
439
|
+
if (screenSharePub) {
|
440
|
+
return;
|
441
|
+
}
|
442
|
+
participant = p;
|
443
|
+
const pub = p.getTrack(Track.Source.ScreenShare);
|
444
|
+
if (pub?.isSubscribed) {
|
445
|
+
screenSharePub = pub;
|
446
|
+
}
|
447
|
+
});
|
354
448
|
} else {
|
355
|
-
|
449
|
+
participant = currentRoom.localParticipant;
|
356
450
|
}
|
357
|
-
isFrontFacing = !isFrontFacing;
|
358
|
-
const options: CreateVideoTrackOptions = {
|
359
|
-
resolution: VideoPresets.qhd.resolution,
|
360
|
-
facingMode: isFrontFacing ? 'user' : 'environment',
|
361
|
-
};
|
362
|
-
videoPub.videoTrack?.restartTrack(options);
|
363
|
-
};
|
364
|
-
|
365
|
-
const defaultDevices = new Map<MediaDeviceKind, string>();
|
366
|
-
window.handleDeviceSelected = async (e: Event) => {
|
367
|
-
const deviceId = (<HTMLSelectElement>e.target).value;
|
368
|
-
const elementId = (<HTMLSelectElement>e.target).id;
|
369
|
-
const kind = elementMapping[elementId];
|
370
|
-
if (!kind) {
|
371
|
-
return;
|
372
|
-
}
|
373
|
-
|
374
|
-
defaultDevices.set(kind, deviceId);
|
375
451
|
|
376
|
-
if (
|
377
|
-
|
452
|
+
if (screenSharePub && participant) {
|
453
|
+
div.style.display = 'block';
|
454
|
+
const videoElm = <HTMLVideoElement>$('screenshare-video');
|
455
|
+
screenSharePub.videoTrack?.attach(videoElm);
|
456
|
+
videoElm.onresize = () => {
|
457
|
+
updateVideoSize(videoElm, <HTMLSpanElement>$('screenshare-resolution'));
|
458
|
+
};
|
459
|
+
const infoElm = $('screenshare-info')!;
|
460
|
+
infoElm.innerHTML = `Screenshare from ${participant.identity}`;
|
461
|
+
} else {
|
462
|
+
div.style.display = 'none';
|
378
463
|
}
|
379
|
-
}
|
464
|
+
}
|
380
465
|
|
381
|
-
|
466
|
+
function updateVideoSize(element: HTMLVideoElement, target: HTMLElement) {
|
467
|
+
target.innerHTML = `(${element.videoWidth}x${element.videoHeight})`;
|
468
|
+
}
|
382
469
|
|
383
|
-
|
384
|
-
const
|
385
|
-
|
386
|
-
if (!videoTrack) {
|
387
|
-
return;
|
388
|
-
}
|
470
|
+
function setButtonState(buttonId: string, buttonText: string, isActive: boolean) {
|
471
|
+
const el = $(buttonId);
|
472
|
+
if (!el) return;
|
389
473
|
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
474
|
+
el.innerHTML = buttonText;
|
475
|
+
if (isActive) {
|
476
|
+
el.classList.add('active');
|
477
|
+
} else {
|
478
|
+
el.classList.remove('active');
|
394
479
|
}
|
395
480
|
}
|
396
481
|
|
482
|
+
setTimeout(handleDevicesChanged, 100);
|
483
|
+
|
397
484
|
function setButtonsForState(connected: boolean) {
|
398
485
|
const connectedSet = [
|
399
486
|
'toggle-video-button',
|
@@ -402,6 +489,7 @@ function setButtonsForState(connected: boolean) {
|
|
402
489
|
'disconnect-ws-button',
|
403
490
|
'disconnect-room-button',
|
404
491
|
'flip-video-button',
|
492
|
+
'send-button',
|
405
493
|
];
|
406
494
|
const disconnectedSet = ['connect-button'];
|
407
495
|
|
@@ -412,10 +500,6 @@ function setButtonsForState(connected: boolean) {
|
|
412
500
|
toAdd.forEach((id) => $(id)?.setAttribute('disabled', 'true'));
|
413
501
|
}
|
414
502
|
|
415
|
-
function getMyVideo() {
|
416
|
-
return <HTMLVideoElement>document.querySelector('#local-video video');
|
417
|
-
}
|
418
|
-
|
419
503
|
const elementMapping: { [k: string]: MediaDeviceKind } = {
|
420
504
|
'video-input': 'videoinput',
|
421
505
|
'audio-input': 'audioinput',
|
@@ -429,7 +513,7 @@ async function handleDevicesChanged() {
|
|
429
513
|
}
|
430
514
|
const devices = await Room.getLocalDevices(kind);
|
431
515
|
const element = <HTMLSelectElement>$(id);
|
432
|
-
populateSelect(kind, element, devices, defaultDevices.get(kind));
|
516
|
+
populateSelect(kind, element, devices, state.defaultDevices.get(kind));
|
433
517
|
}));
|
434
518
|
}
|
435
519
|
|