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.
Files changed (2) hide show
  1. package/package.json +2 -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.0.3",
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.1"
18
+ "node-fetch": "^3.2.3"
19
19
  },
20
20
  "funding": {
21
21
  "type": "individual",
@@ -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, polite, sendMessage, localTracks, debug) {
147
+ constructor(pcConfig, caller, sendMessage, localTracks, debug) {
152
148
  super();
153
149
  this._pcConfig = pcConfig;
154
- this._polite = polite;
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._polite ? '' : 'im'}polite peer`);
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) await this._setRemoteDescription(description);
378
- if (candidate != null) await this._addIceCandidate(candidate);
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 isPolite = (myId, otherId) => {
400
- // Compare user IDs to determine which of the two users is the "polite" user.
401
- const polite = myId.localeCompare(otherId) < 0;
402
- if ((otherId.localeCompare(myId) < 0) === polite) {
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 polite;
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('muted', isLocal); // Setting the 'muted' attribute isn't sufficient for some reason.
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
- isPolite(this.getUserId(), userId),
1258
+ isCaller(this.getUserId(), userId),
1293
1259
  (msg) => this.sendMessage(userId, msg),
1294
1260
  this._localTracks,
1295
1261
  _debug);