ep_webrtc 2.0.4 → 2.2.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/package.json +2 -2
- package/static/js/index.js +83 -113
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"url": "git@github.com:ether/ep_webrtc.git",
|
|
6
6
|
"type": "git"
|
|
7
7
|
},
|
|
8
|
-
"version": "2.0
|
|
8
|
+
"version": "2.2.0",
|
|
9
9
|
"description": "WebRTC based audio/video chat to Etherpad",
|
|
10
10
|
"author": "John McLear <john@mclear.co.uk>",
|
|
11
11
|
"contributors": [],
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"abort-controller": "^3.0.0",
|
|
17
17
|
"lodash": "^4.17.21",
|
|
18
|
-
"node-fetch": "^3.2.
|
|
18
|
+
"node-fetch": "^3.2.3"
|
|
19
19
|
},
|
|
20
20
|
"funding": {
|
|
21
21
|
"type": "individual",
|
package/static/js/index.js
CHANGED
|
@@ -87,7 +87,7 @@ class LocalTracks extends EventTargetPolyfill {
|
|
|
87
87
|
this.videoIsScreenshare = false;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
getTracks(kind) {
|
|
91
91
|
return kind === 'audio' ? this.stream.getAudioTracks()
|
|
92
92
|
: kind === 'video' ? this.stream.getVideoTracks()
|
|
93
93
|
: this.stream.getTracks();
|
|
@@ -96,7 +96,7 @@ class LocalTracks extends EventTargetPolyfill {
|
|
|
96
96
|
setTrack(kind, newTrack) {
|
|
97
97
|
newTrack = newTrack || null; // Convert undefined to null.
|
|
98
98
|
let oldTrack = null;
|
|
99
|
-
const tracks = this.
|
|
99
|
+
const tracks = this.getTracks(kind);
|
|
100
100
|
for (const track of tracks) {
|
|
101
101
|
if (track.kind !== kind) continue;
|
|
102
102
|
if (track === newTrack) return; // No change.
|
|
@@ -109,7 +109,7 @@ class LocalTracks extends EventTargetPolyfill {
|
|
|
109
109
|
debug(`adding ${kind} track ${newTrack.id} to local stream`);
|
|
110
110
|
newTrack.addEventListener('ended', () => {
|
|
111
111
|
debug(`local ${kind} track ${newTrack.id} ended`);
|
|
112
|
-
if (!this.
|
|
112
|
+
if (!this.getTracks(kind).includes(newTrack)) return;
|
|
113
113
|
this.setTrack(kind, null);
|
|
114
114
|
});
|
|
115
115
|
this.stream.addTrack(newTrack);
|
|
@@ -135,10 +135,6 @@ class ClosedEvent extends CustomEvent {
|
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
// The WebRTC negotiation logic used here is based on the "Perfect Negotiation Example" at
|
|
139
|
-
// https://www.w3.org/TR/2021/REC-webrtc-20210126/#perfect-negotiation-example. See there for
|
|
140
|
-
// details about how it works.
|
|
141
|
-
//
|
|
142
138
|
// Events:
|
|
143
139
|
// * 'stream' (see StreamEvent): Emitted when the remote stream is ready. For every 'stream' event
|
|
144
140
|
// there will be a corresponding 'streamgone' event. Once a stream is added another stream will
|
|
@@ -148,15 +144,15 @@ class ClosedEvent extends CustomEvent {
|
|
|
148
144
|
// * 'closed' (see ClosedEvent): Emitted when the PeerState is closed, except when closed by a
|
|
149
145
|
// call to close(). The PeerState must not be used after it is closed.
|
|
150
146
|
class PeerState extends EventTargetPolyfill {
|
|
151
|
-
constructor(pcConfig,
|
|
147
|
+
constructor(pcConfig, caller, sendMessage, localTracks, debug) {
|
|
152
148
|
super();
|
|
153
149
|
this._pcConfig = pcConfig;
|
|
154
|
-
this.
|
|
150
|
+
this._caller = caller;
|
|
155
151
|
this._sendMessage = (msg) => sendMessage(Object.assign({ids: this._ids}, msg));
|
|
156
152
|
this._localTracks = localTracks;
|
|
157
153
|
this._failedSLDAttempts = 0;
|
|
158
154
|
this._debug = debug;
|
|
159
|
-
this._debug(`I am the ${this.
|
|
155
|
+
this._debug(`I am the ${this._caller ? 'calling' : 'answering'} peer`);
|
|
160
156
|
this._ids = {
|
|
161
157
|
from: {
|
|
162
158
|
// Only changes when the user reloads the page.
|
|
@@ -167,45 +163,42 @@ class PeerState extends EventTargetPolyfill {
|
|
|
167
163
|
};
|
|
168
164
|
this._closed = false;
|
|
169
165
|
this._pc = null;
|
|
166
|
+
this._xcvrs = {};
|
|
170
167
|
this._remoteStream = null;
|
|
171
|
-
this._onremovetrack =
|
|
172
|
-
() => { if (this._remoteStream.getTracks().length === 0) this._setRemoteStream(null); };
|
|
173
168
|
this._ontrackchanged = async ({oldTrack, newTrack}) => {
|
|
174
|
-
|
|
175
|
-
this.
|
|
176
|
-
`${oldTrack ? oldTrack.id : '(null)'} with ` +
|
|
177
|
-
`${newTrack ? newTrack.id : '(null)'}`);
|
|
178
|
-
if (oldTrack != null) {
|
|
179
|
-
for (const sender of this._pc.getSenders()) {
|
|
180
|
-
if (sender.track !== oldTrack) continue;
|
|
181
|
-
if (newTrack != null) {
|
|
182
|
-
try {
|
|
183
|
-
return await sender.replaceTrack(newTrack);
|
|
184
|
-
} catch (err) {
|
|
185
|
-
this._debug('renegotiation is required');
|
|
186
|
-
logErrorToServer(err);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
this._pc.removeTrack(sender);
|
|
190
|
-
break;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
if (newTrack != null) this._pc.addTrack(newTrack, this._localTracks.stream);
|
|
169
|
+
const {kind} = oldTrack || newTrack;
|
|
170
|
+
await this._sendLocalTrack(kind, newTrack);
|
|
194
171
|
};
|
|
195
172
|
this._localTracks.addEventListener('trackchanged', this._ontrackchanged);
|
|
196
173
|
}
|
|
197
174
|
|
|
175
|
+
async _sendLocalTrack(kind, track) {
|
|
176
|
+
if (this._pc == null || this._pc.connectionState === 'closed') return;
|
|
177
|
+
const {sender: {track: old} = {}} = this._xcvrs[kind] || {};
|
|
178
|
+
if ((old == null && track == null) || old === track) return;
|
|
179
|
+
this._debug(
|
|
180
|
+
`replacing ${kind} track ${old ? old.id : '(null)'} with ${track ? track.id : '(null)'}`);
|
|
181
|
+
const xcvr = this._xcvrs[kind];
|
|
182
|
+
if (!xcvr) return;
|
|
183
|
+
try {
|
|
184
|
+
if (track != null) xcvr.direction = 'sendrecv'; // Will throw if not already bidirectional.
|
|
185
|
+
await xcvr.sender.replaceTrack(track);
|
|
186
|
+
} catch (err) {
|
|
187
|
+
this._debug('renegotiation is required');
|
|
188
|
+
this._resetConnection(err);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
198
193
|
_setRemoteStream(stream) {
|
|
199
194
|
if (stream === this._remoteStream) return;
|
|
200
195
|
if (this._remoteStream != null) {
|
|
201
196
|
const oldStream = this._remoteStream;
|
|
202
|
-
oldStream.removeEventListener('removetrack', this._onremovetrack);
|
|
203
197
|
this._remoteStream = null;
|
|
204
198
|
this.dispatchEvent(new StreamEvent('streamgone', oldStream));
|
|
205
199
|
}
|
|
206
200
|
if (stream != null) {
|
|
207
201
|
this._remoteStream = stream;
|
|
208
|
-
stream.addEventListener('removetrack', this._onremovetrack);
|
|
209
202
|
this.dispatchEvent(new StreamEvent('stream', stream));
|
|
210
203
|
}
|
|
211
204
|
}
|
|
@@ -224,24 +217,21 @@ class PeerState extends EventTargetPolyfill {
|
|
|
224
217
|
this._setRemoteStream(null);
|
|
225
218
|
this._ids.from.instance = ++nextInstanceId;
|
|
226
219
|
this._ids.to = peerIds;
|
|
227
|
-
// This negotiation state is captured in closures (instead of doing something like
|
|
228
|
-
// this._negotiationState) because this._resetConnection() needs to be sure that all of the
|
|
229
|
-
// event handlers for the old RTCPeerConnection do not mutate the negotiation state for the new
|
|
230
|
-
// RTCPeerConnection.
|
|
231
|
-
const negotiationState = {
|
|
232
|
-
makingOffer: false,
|
|
233
|
-
ignoreOffer: false,
|
|
234
|
-
isSettingRemoteAnswerPending: false,
|
|
235
|
-
};
|
|
236
220
|
const pc = new RTCPeerConnection(this._pcConfig);
|
|
237
|
-
pc.addEventListener('track', ({track,
|
|
238
|
-
|
|
239
|
-
this.
|
|
221
|
+
pc.addEventListener('track', async ({track, transceiver}) => {
|
|
222
|
+
this._debug(`Received ${track.kind} track from peer, ID: ${track.id}`);
|
|
223
|
+
const stream = this._remoteStream || new MediaStream();
|
|
224
|
+
stream.addTrack(track);
|
|
225
|
+
this._setRemoteStream(stream);
|
|
226
|
+
if (!this._caller) {
|
|
227
|
+
this._xcvrs[track.kind] = transceiver;
|
|
228
|
+
// _sendLocalTrack might call _resetConnection, so it should be called last.
|
|
229
|
+
await this._sendLocalTrack(track.kind, this._localTracks.getTracks(track.kind)[0]);
|
|
230
|
+
}
|
|
240
231
|
});
|
|
241
232
|
pc.addEventListener('icecandidate', ({candidate}) => this._sendMessage({candidate}));
|
|
242
233
|
pc.addEventListener('negotiationneeded', async () => {
|
|
243
234
|
try {
|
|
244
|
-
negotiationState.makingOffer = true;
|
|
245
235
|
await pc.setLocalDescription();
|
|
246
236
|
this._failedSLDAttempts = 0;
|
|
247
237
|
this._sendMessage({description: pc.localDescription});
|
|
@@ -250,8 +240,6 @@ class PeerState extends EventTargetPolyfill {
|
|
|
250
240
|
if (++this._failedSLDAttempts > 10) throw err; // Avoid an infinite loop.
|
|
251
241
|
this._resetConnection(err);
|
|
252
242
|
return;
|
|
253
|
-
} finally {
|
|
254
|
-
negotiationState.makingOffer = false;
|
|
255
243
|
}
|
|
256
244
|
});
|
|
257
245
|
pc.addEventListener('connectionstatechange', () => {
|
|
@@ -299,46 +287,33 @@ class PeerState extends EventTargetPolyfill {
|
|
|
299
287
|
|
|
300
288
|
if (this._pc != null) this._pc.close();
|
|
301
289
|
this._pc = pc;
|
|
302
|
-
this.
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
this.
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
};
|
|
330
|
-
|
|
331
|
-
const tracks = this._localTracks.stream.getTracks();
|
|
332
|
-
for (const track of tracks) {
|
|
333
|
-
this._debug(`start streaming ${track.kind} track ${track.id}`);
|
|
334
|
-
pc.addTrack(track, this._localTracks.stream);
|
|
290
|
+
this._xcvrs = {};
|
|
291
|
+
|
|
292
|
+
if (this._caller) {
|
|
293
|
+
// Adding transceivers triggers negotiation with the peer, which we want to do as soon as
|
|
294
|
+
// possible to warm up the peer connection (ICE takes a while). Adding a track implicitly adds
|
|
295
|
+
// a transceiver so we could do that instead, but:
|
|
296
|
+
//
|
|
297
|
+
// * We might not have a local track yet (maybe the user hasn't yet allowed access to the
|
|
298
|
+
// camera/mic).
|
|
299
|
+
// * Using a dummy silence track until a real track is ready might result in two
|
|
300
|
+
// unidirectional SDP m-lines (one in each direction) instead of a single bidirectional
|
|
301
|
+
// m-line. This seems to trigger an echo bug in Safari.
|
|
302
|
+
//
|
|
303
|
+
// Using transceivers to warm up the connection is the approach taken in:
|
|
304
|
+
// https://www.w3.org/TR/2021/REC-webrtc-20210126/#advanced-peer-to-peer-example-with-warm-up
|
|
305
|
+
// Also see: https://webrtcstandards.info/ice-warm-up-m-line-webrtc-transceivers/
|
|
306
|
+
Promise.all(['audio', 'video'].map(async (t) => {
|
|
307
|
+
this._debug(`adding ${t} transceiver`);
|
|
308
|
+
this._xcvrs[t] = this._pc.addTransceiver(t);
|
|
309
|
+
await this._sendLocalTrack(t, this._localTracks.getTracks(t)[0]);
|
|
310
|
+
}));
|
|
311
|
+
} else {
|
|
312
|
+
// It is possible that the last invite sent to the peer was sent before the peer was ready to
|
|
313
|
+
// accept invites, so the peer might not know that it should call now. Send another invite
|
|
314
|
+
// just in case. The peer will ignore any superfluous invites.
|
|
315
|
+
this._sendMessage({invite: 'invite'});
|
|
335
316
|
}
|
|
336
|
-
// Creating an RTCPeerConnection doesn't actually generate any control messages until
|
|
337
|
-
// RTCPeerConnection.addTrack() is called. It is possible that the last invite sent to the peer
|
|
338
|
-
// was sent before the peer was ready to accept invites. If that is the case and there are no
|
|
339
|
-
// local tracks to trigger a WebRTC message exchange, the peer won't know that it is OK to
|
|
340
|
-
// connect back. Send another invite just in case. The peer will ignore any superfluous invites.
|
|
341
|
-
if (tracks.length === 0) this._sendMessage({invite: 'invite'});
|
|
342
317
|
}
|
|
343
318
|
|
|
344
319
|
async receiveMessage(msg) {
|
|
@@ -374,8 +349,14 @@ class PeerState extends EventTargetPolyfill {
|
|
|
374
349
|
}
|
|
375
350
|
if (this._pc == null) this._resetConnection(null, this._ids.to);
|
|
376
351
|
try {
|
|
377
|
-
if (description != null)
|
|
378
|
-
|
|
352
|
+
if (description != null) {
|
|
353
|
+
await this._pc.setRemoteDescription(description);
|
|
354
|
+
if (description.type === 'offer') {
|
|
355
|
+
await this._pc.setLocalDescription();
|
|
356
|
+
this._sendMessage({description: this._pc.localDescription});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (candidate != null) await this._pc.addIceCandidate(candidate);
|
|
379
360
|
} catch (err) {
|
|
380
361
|
err.peerMessage = msg;
|
|
381
362
|
console.error('Error processing message from peer:', err);
|
|
@@ -396,14 +377,13 @@ class PeerState extends EventTargetPolyfill {
|
|
|
396
377
|
}
|
|
397
378
|
}
|
|
398
379
|
|
|
399
|
-
const
|
|
400
|
-
// Compare user IDs to determine which of the two users
|
|
401
|
-
const
|
|
402
|
-
if ((otherId.localeCompare(myId) < 0) ===
|
|
403
|
-
// One peer must be polite and the other must be impolite.
|
|
380
|
+
const isCaller = (myId, otherId) => {
|
|
381
|
+
// Compare user IDs to determine which of the two users will initiate the call.
|
|
382
|
+
const caller = myId.localeCompare(otherId) < 0;
|
|
383
|
+
if ((otherId.localeCompare(myId) < 0) === caller) {
|
|
404
384
|
throw new Error(`Peer ID ${otherId} compares equivalent to own ID ${myId}`);
|
|
405
385
|
}
|
|
406
|
-
return
|
|
386
|
+
return caller;
|
|
407
387
|
};
|
|
408
388
|
|
|
409
389
|
// Periods in element IDs make it hard to build a selector string because period is for class match.
|
|
@@ -422,17 +402,7 @@ exports.rtc = new class {
|
|
|
422
402
|
constructor() {
|
|
423
403
|
this._activated = null;
|
|
424
404
|
this._settings = null;
|
|
425
|
-
this._disabledSilence = (() => {
|
|
426
|
-
const ctx = new AudioContext();
|
|
427
|
-
const gain = ctx.createGain();
|
|
428
|
-
const dst = gain.connect(ctx.createMediaStreamDestination());
|
|
429
|
-
const track = dst.stream.getAudioTracks()[0];
|
|
430
|
-
track.enabled = false;
|
|
431
|
-
return track;
|
|
432
|
-
})();
|
|
433
|
-
this._blankStream = new MediaStream([this._disabledSilence]);
|
|
434
405
|
this._localTracks = new LocalTracks();
|
|
435
|
-
this._localTracks.setTrack(this._disabledSilence.kind, this._disabledSilence);
|
|
436
406
|
this._localTracks.addEventListener('trackchanged', ({oldTrack, newTrack}) => {
|
|
437
407
|
// Normally the self-view UI only needs to be updated if the user clicks on something, but it
|
|
438
408
|
// also needs to be updated if the browser decides to end the local stream for whatever
|
|
@@ -657,8 +627,7 @@ exports.rtc = new class {
|
|
|
657
627
|
try {
|
|
658
628
|
const devices = [];
|
|
659
629
|
const addAudioTrack = updateAudio && this._selfViewButtons.audio.enabled &&
|
|
660
|
-
!this._localTracks.stream.getAudioTracks().some(
|
|
661
|
-
(t) => t !== this._disabledSilence && t.readyState === 'live');
|
|
630
|
+
!this._localTracks.stream.getAudioTracks().some((t) => t.readyState === 'live');
|
|
662
631
|
if (addAudioTrack) devices.push('mic');
|
|
663
632
|
const addVideoTrack = updateVideo && this._selfViewButtons.video.enabled &&
|
|
664
633
|
(!this._localTracks.stream.getVideoTracks().some((t) => t.readyState === 'live') ||
|
|
@@ -711,7 +680,7 @@ exports.rtc = new class {
|
|
|
711
680
|
for (const track of this._localTracks.stream.getAudioTracks()) {
|
|
712
681
|
// Re-check the state of the button because the user might have clicked it while
|
|
713
682
|
// getUserMedia() was running.
|
|
714
|
-
track.enabled =
|
|
683
|
+
track.enabled = this._selfViewButtons.audio.enabled;
|
|
715
684
|
}
|
|
716
685
|
const hasAudio = this._localTracks.stream.getAudioTracks().some(
|
|
717
686
|
(t) => t.enabled && t.readyState === 'live');
|
|
@@ -917,8 +886,9 @@ exports.rtc = new class {
|
|
|
917
886
|
minWidth: iw > width ? `${iw}px` : '',
|
|
918
887
|
minHeight: nh + ih > height ? `${nh + ih}px` : '',
|
|
919
888
|
});
|
|
920
|
-
})
|
|
921
|
-
|
|
889
|
+
});
|
|
890
|
+
if (isLocal) $videoContainer.prependTo($('#rtcbox'));
|
|
891
|
+
else $videoContainer.appendTo($('#rtcbox'));
|
|
922
892
|
this.updatePeerNameAndColor(this.getUserFromId(userId));
|
|
923
893
|
|
|
924
894
|
// For tests it is important to know when an asynchronous event handler has finishing handling
|
|
@@ -1292,7 +1262,7 @@ exports.rtc = new class {
|
|
|
1292
1262
|
_debug('creating PeerState');
|
|
1293
1263
|
peer = new PeerState(
|
|
1294
1264
|
{iceServers: this._settings.iceServers},
|
|
1295
|
-
|
|
1265
|
+
isCaller(this.getUserId(), userId),
|
|
1296
1266
|
(msg) => this.sendMessage(userId, msg),
|
|
1297
1267
|
this._localTracks,
|
|
1298
1268
|
_debug);
|
|
@@ -1315,7 +1285,7 @@ exports.rtc = new class {
|
|
|
1315
1285
|
logDisconnectErrorTimeout =
|
|
1316
1286
|
setTimeout(() => logErrorToServer(new Error('RTC connection lost'), 0), 10000);
|
|
1317
1287
|
($videoContainer.data('updateMinSize') || (() => {}))();
|
|
1318
|
-
await this.setStream(userId,
|
|
1288
|
+
await this.setStream(userId, new MediaStream());
|
|
1319
1289
|
});
|
|
1320
1290
|
peer.addEventListener('closed', () => {
|
|
1321
1291
|
_debug('PeerState closed');
|