ep_webrtc 2.0.3 → 2.1.1
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 +29 -63
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.
|
|
8
|
+
"version": "2.1.1",
|
|
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
|
@@ -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.
|
|
@@ -224,15 +220,6 @@ class PeerState extends EventTargetPolyfill {
|
|
|
224
220
|
this._setRemoteStream(null);
|
|
225
221
|
this._ids.from.instance = ++nextInstanceId;
|
|
226
222
|
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
223
|
const pc = new RTCPeerConnection(this._pcConfig);
|
|
237
224
|
pc.addEventListener('track', ({track, streams}) => {
|
|
238
225
|
if (streams.length !== 1) throw new Error('Expected track to be in exactly one stream');
|
|
@@ -240,8 +227,15 @@ class PeerState extends EventTargetPolyfill {
|
|
|
240
227
|
});
|
|
241
228
|
pc.addEventListener('icecandidate', ({candidate}) => this._sendMessage({candidate}));
|
|
242
229
|
pc.addEventListener('negotiationneeded', async () => {
|
|
230
|
+
if (!this._caller) {
|
|
231
|
+
this._debug('Waiting for peer to call');
|
|
232
|
+
// It is possible that the last invite sent to the peer was sent before the peer was ready
|
|
233
|
+
// to accept invites, so the peer might not know that it should call now. Send another
|
|
234
|
+
// invite just in case. The peer will ignore any superfluous invites.
|
|
235
|
+
this._sendMessage({invite: 'invite'});
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
243
238
|
try {
|
|
244
|
-
negotiationState.makingOffer = true;
|
|
245
239
|
await pc.setLocalDescription();
|
|
246
240
|
this._failedSLDAttempts = 0;
|
|
247
241
|
this._sendMessage({description: pc.localDescription});
|
|
@@ -250,8 +244,6 @@ class PeerState extends EventTargetPolyfill {
|
|
|
250
244
|
if (++this._failedSLDAttempts > 10) throw err; // Avoid an infinite loop.
|
|
251
245
|
this._resetConnection(err);
|
|
252
246
|
return;
|
|
253
|
-
} finally {
|
|
254
|
-
negotiationState.makingOffer = false;
|
|
255
247
|
}
|
|
256
248
|
});
|
|
257
249
|
pc.addEventListener('connectionstatechange', () => {
|
|
@@ -299,46 +291,12 @@ class PeerState extends EventTargetPolyfill {
|
|
|
299
291
|
|
|
300
292
|
if (this._pc != null) this._pc.close();
|
|
301
293
|
this._pc = pc;
|
|
302
|
-
this._setRemoteDescription = async (description) => {
|
|
303
|
-
const readyForOffer = !negotiationState.makingOffer &&
|
|
304
|
-
(pc.signalingState === 'stable' || negotiationState.isSettingRemoteAnswerPending);
|
|
305
|
-
const offerCollision = description.type === 'offer' && !readyForOffer;
|
|
306
|
-
negotiationState.ignoreOffer = !this._polite && offerCollision;
|
|
307
|
-
if (negotiationState.ignoreOffer) {
|
|
308
|
-
this._debug('ignoring offer due to offer collision');
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
negotiationState.isSettingRemoteAnswerPending = description.type === 'answer';
|
|
312
|
-
await pc.setRemoteDescription(description);
|
|
313
|
-
// The "Perfect Negotiation Example" doesn't put this next line inside a `finally` block. It
|
|
314
|
-
// is unclear whether that is intentional. Fortunately it doesn't matter here: If the above
|
|
315
|
-
// pc.setRemoteDescription() call throws, _resetConnection() is called to restart the
|
|
316
|
-
// negotiation anyway.
|
|
317
|
-
negotiationState.isSettingRemoteAnswerPending = false;
|
|
318
|
-
if (description.type === 'offer') {
|
|
319
|
-
await pc.setLocalDescription();
|
|
320
|
-
this._sendMessage({description: pc.localDescription});
|
|
321
|
-
}
|
|
322
|
-
};
|
|
323
|
-
this._addIceCandidate = async (candidate) => {
|
|
324
|
-
try {
|
|
325
|
-
await pc.addIceCandidate(candidate);
|
|
326
|
-
} catch (err) {
|
|
327
|
-
if (!negotiationState.ignoreOffer) throw err;
|
|
328
|
-
}
|
|
329
|
-
};
|
|
330
294
|
|
|
331
295
|
const tracks = this._localTracks.stream.getTracks();
|
|
332
296
|
for (const track of tracks) {
|
|
333
297
|
this._debug(`start streaming ${track.kind} track ${track.id}`);
|
|
334
298
|
pc.addTrack(track, this._localTracks.stream);
|
|
335
299
|
}
|
|
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
300
|
}
|
|
343
301
|
|
|
344
302
|
async receiveMessage(msg) {
|
|
@@ -374,8 +332,14 @@ class PeerState extends EventTargetPolyfill {
|
|
|
374
332
|
}
|
|
375
333
|
if (this._pc == null) this._resetConnection(null, this._ids.to);
|
|
376
334
|
try {
|
|
377
|
-
if (description != null)
|
|
378
|
-
|
|
335
|
+
if (description != null) {
|
|
336
|
+
await this._pc.setRemoteDescription(description);
|
|
337
|
+
if (description.type === 'offer') {
|
|
338
|
+
await this._pc.setLocalDescription();
|
|
339
|
+
this._sendMessage({description: this._pc.localDescription});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (candidate != null) await this._pc.addIceCandidate(candidate);
|
|
379
343
|
} catch (err) {
|
|
380
344
|
err.peerMessage = msg;
|
|
381
345
|
console.error('Error processing message from peer:', err);
|
|
@@ -396,14 +360,13 @@ class PeerState extends EventTargetPolyfill {
|
|
|
396
360
|
}
|
|
397
361
|
}
|
|
398
362
|
|
|
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.
|
|
363
|
+
const isCaller = (myId, otherId) => {
|
|
364
|
+
// Compare user IDs to determine which of the two users will initiate the call.
|
|
365
|
+
const caller = myId.localeCompare(otherId) < 0;
|
|
366
|
+
if ((otherId.localeCompare(myId) < 0) === caller) {
|
|
404
367
|
throw new Error(`Peer ID ${otherId} compares equivalent to own ID ${myId}`);
|
|
405
368
|
}
|
|
406
|
-
return
|
|
369
|
+
return caller;
|
|
407
370
|
};
|
|
408
371
|
|
|
409
372
|
// Periods in element IDs make it hard to build a selector string because period is for class match.
|
|
@@ -886,7 +849,10 @@ exports.rtc = new class {
|
|
|
886
849
|
autoplay: '',
|
|
887
850
|
muted: isLocal ? '' : null,
|
|
888
851
|
})
|
|
889
|
-
.prop(
|
|
852
|
+
.prop({
|
|
853
|
+
muted: isLocal, // Setting the 'muted' attribute isn't sufficient for some reason.
|
|
854
|
+
volume: isLocal ? 0.0 : 1.0, // Long shot attempt at fixing echo in Safari.
|
|
855
|
+
});
|
|
890
856
|
const $interface = $('<div>')
|
|
891
857
|
.addClass('interface-container')
|
|
892
858
|
.attr('id', `interface_${videoId}`);
|
|
@@ -1289,7 +1255,7 @@ exports.rtc = new class {
|
|
|
1289
1255
|
_debug('creating PeerState');
|
|
1290
1256
|
peer = new PeerState(
|
|
1291
1257
|
{iceServers: this._settings.iceServers},
|
|
1292
|
-
|
|
1258
|
+
isCaller(this.getUserId(), userId),
|
|
1293
1259
|
(msg) => this.sendMessage(userId, msg),
|
|
1294
1260
|
this._localTracks,
|
|
1295
1261
|
_debug);
|