ep_webrtc 2.1.1 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/static/js/index.js +66 -59
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.1.1",
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": [],
@@ -87,7 +87,7 @@ class LocalTracks extends EventTargetPolyfill {
87
87
  this.videoIsScreenshare = false;
88
88
  }
89
89
 
90
- _getTracks(kind) {
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._getTracks(kind);
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._getTracks(kind).includes(newTrack)) return;
112
+ if (!this.getTracks(kind).includes(newTrack)) return;
113
113
  this.setTrack(kind, null);
114
114
  });
115
115
  this.stream.addTrack(newTrack);
@@ -163,45 +163,42 @@ class PeerState extends EventTargetPolyfill {
163
163
  };
164
164
  this._closed = false;
165
165
  this._pc = null;
166
+ this._xcvrs = {};
166
167
  this._remoteStream = null;
167
- this._onremovetrack =
168
- () => { if (this._remoteStream.getTracks().length === 0) this._setRemoteStream(null); };
169
168
  this._ontrackchanged = async ({oldTrack, newTrack}) => {
170
- if (this._pc == null || this._pc.connectionState === 'closed') return;
171
- this._debug(`replacing ${oldTrack ? oldTrack.kind : newTrack.kind} track ` +
172
- `${oldTrack ? oldTrack.id : '(null)'} with ` +
173
- `${newTrack ? newTrack.id : '(null)'}`);
174
- if (oldTrack != null) {
175
- for (const sender of this._pc.getSenders()) {
176
- if (sender.track !== oldTrack) continue;
177
- if (newTrack != null) {
178
- try {
179
- return await sender.replaceTrack(newTrack);
180
- } catch (err) {
181
- this._debug('renegotiation is required');
182
- logErrorToServer(err);
183
- }
184
- }
185
- this._pc.removeTrack(sender);
186
- break;
187
- }
188
- }
189
- if (newTrack != null) this._pc.addTrack(newTrack, this._localTracks.stream);
169
+ const {kind} = oldTrack || newTrack;
170
+ await this._sendLocalTrack(kind, newTrack);
190
171
  };
191
172
  this._localTracks.addEventListener('trackchanged', this._ontrackchanged);
192
173
  }
193
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
+
194
193
  _setRemoteStream(stream) {
195
194
  if (stream === this._remoteStream) return;
196
195
  if (this._remoteStream != null) {
197
196
  const oldStream = this._remoteStream;
198
- oldStream.removeEventListener('removetrack', this._onremovetrack);
199
197
  this._remoteStream = null;
200
198
  this.dispatchEvent(new StreamEvent('streamgone', oldStream));
201
199
  }
202
200
  if (stream != null) {
203
201
  this._remoteStream = stream;
204
- stream.addEventListener('removetrack', this._onremovetrack);
205
202
  this.dispatchEvent(new StreamEvent('stream', stream));
206
203
  }
207
204
  }
@@ -221,20 +218,19 @@ class PeerState extends EventTargetPolyfill {
221
218
  this._ids.from.instance = ++nextInstanceId;
222
219
  this._ids.to = peerIds;
223
220
  const pc = new RTCPeerConnection(this._pcConfig);
224
- pc.addEventListener('track', ({track, streams}) => {
225
- if (streams.length !== 1) throw new Error('Expected track to be in exactly one stream');
226
- this._setRemoteStream(streams[0]);
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
+ }
227
231
  });
228
232
  pc.addEventListener('icecandidate', ({candidate}) => this._sendMessage({candidate}));
229
233
  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
- }
238
234
  try {
239
235
  await pc.setLocalDescription();
240
236
  this._failedSLDAttempts = 0;
@@ -291,11 +287,32 @@ class PeerState extends EventTargetPolyfill {
291
287
 
292
288
  if (this._pc != null) this._pc.close();
293
289
  this._pc = pc;
294
-
295
- const tracks = this._localTracks.stream.getTracks();
296
- for (const track of tracks) {
297
- this._debug(`start streaming ${track.kind} track ${track.id}`);
298
- 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'});
299
316
  }
300
317
  }
301
318
 
@@ -385,17 +402,7 @@ exports.rtc = new class {
385
402
  constructor() {
386
403
  this._activated = null;
387
404
  this._settings = null;
388
- this._disabledSilence = (() => {
389
- const ctx = new AudioContext();
390
- const gain = ctx.createGain();
391
- const dst = gain.connect(ctx.createMediaStreamDestination());
392
- const track = dst.stream.getAudioTracks()[0];
393
- track.enabled = false;
394
- return track;
395
- })();
396
- this._blankStream = new MediaStream([this._disabledSilence]);
397
405
  this._localTracks = new LocalTracks();
398
- this._localTracks.setTrack(this._disabledSilence.kind, this._disabledSilence);
399
406
  this._localTracks.addEventListener('trackchanged', ({oldTrack, newTrack}) => {
400
407
  // Normally the self-view UI only needs to be updated if the user clicks on something, but it
401
408
  // also needs to be updated if the browser decides to end the local stream for whatever
@@ -620,8 +627,7 @@ exports.rtc = new class {
620
627
  try {
621
628
  const devices = [];
622
629
  const addAudioTrack = updateAudio && this._selfViewButtons.audio.enabled &&
623
- !this._localTracks.stream.getAudioTracks().some(
624
- (t) => t !== this._disabledSilence && t.readyState === 'live');
630
+ !this._localTracks.stream.getAudioTracks().some((t) => t.readyState === 'live');
625
631
  if (addAudioTrack) devices.push('mic');
626
632
  const addVideoTrack = updateVideo && this._selfViewButtons.video.enabled &&
627
633
  (!this._localTracks.stream.getVideoTracks().some((t) => t.readyState === 'live') ||
@@ -674,7 +680,7 @@ exports.rtc = new class {
674
680
  for (const track of this._localTracks.stream.getAudioTracks()) {
675
681
  // Re-check the state of the button because the user might have clicked it while
676
682
  // getUserMedia() was running.
677
- track.enabled = track !== this._disabledSilence && this._selfViewButtons.audio.enabled;
683
+ track.enabled = this._selfViewButtons.audio.enabled;
678
684
  }
679
685
  const hasAudio = this._localTracks.stream.getAudioTracks().some(
680
686
  (t) => t.enabled && t.readyState === 'live');
@@ -880,8 +886,9 @@ exports.rtc = new class {
880
886
  minWidth: iw > width ? `${iw}px` : '',
881
887
  minHeight: nh + ih > height ? `${nh + ih}px` : '',
882
888
  });
883
- })
884
- .appendTo($('#rtcbox'));
889
+ });
890
+ if (isLocal) $videoContainer.prependTo($('#rtcbox'));
891
+ else $videoContainer.appendTo($('#rtcbox'));
885
892
  this.updatePeerNameAndColor(this.getUserFromId(userId));
886
893
 
887
894
  // For tests it is important to know when an asynchronous event handler has finishing handling
@@ -1278,7 +1285,7 @@ exports.rtc = new class {
1278
1285
  logDisconnectErrorTimeout =
1279
1286
  setTimeout(() => logErrorToServer(new Error('RTC connection lost'), 0), 10000);
1280
1287
  ($videoContainer.data('updateMinSize') || (() => {}))();
1281
- await this.setStream(userId, this._blankStream);
1288
+ await this.setStream(userId, new MediaStream());
1282
1289
  });
1283
1290
  peer.addEventListener('closed', () => {
1284
1291
  _debug('PeerState closed');