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