ep_webrtc 2.1.0 → 2.2.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 +4 -4
- package/static/js/index.js +66 -60
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
|
|
8
|
+
"version": "2.2.1",
|
|
9
9
|
"description": "WebRTC based audio/video chat to Etherpad",
|
|
10
10
|
"author": "John McLear <john@mclear.co.uk>",
|
|
11
11
|
"contributors": [],
|
|
@@ -15,15 +15,15 @@
|
|
|
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",
|
|
22
22
|
"url": "https://etherpad.org/"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"eslint": "^8.
|
|
26
|
-
"eslint-config-etherpad": "^3.0.
|
|
25
|
+
"eslint": "^8.11.0",
|
|
26
|
+
"eslint-config-etherpad": "^3.0.9",
|
|
27
27
|
"typescript": "^4.6.2"
|
|
28
28
|
},
|
|
29
29
|
"scripts": {
|
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);
|
|
@@ -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
|
-
|
|
171
|
-
this.
|
|
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,21 +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,
|
|
225
|
-
|
|
226
|
-
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
|
+
}
|
|
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. If that is the case and there are no local tracks to trigger a WebRTC
|
|
234
|
-
// message exchange, the peer won't know that it is OK to connect back. Send another invite
|
|
235
|
-
// just in case. The peer will ignore any superfluous invites.
|
|
236
|
-
this._sendMessage({invite: 'invite'});
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
234
|
try {
|
|
240
235
|
await pc.setLocalDescription();
|
|
241
236
|
this._failedSLDAttempts = 0;
|
|
@@ -292,11 +287,32 @@ class PeerState extends EventTargetPolyfill {
|
|
|
292
287
|
|
|
293
288
|
if (this._pc != null) this._pc.close();
|
|
294
289
|
this._pc = pc;
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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'});
|
|
300
316
|
}
|
|
301
317
|
}
|
|
302
318
|
|
|
@@ -386,17 +402,7 @@ exports.rtc = new class {
|
|
|
386
402
|
constructor() {
|
|
387
403
|
this._activated = null;
|
|
388
404
|
this._settings = null;
|
|
389
|
-
this._disabledSilence = (() => {
|
|
390
|
-
const ctx = new AudioContext();
|
|
391
|
-
const gain = ctx.createGain();
|
|
392
|
-
const dst = gain.connect(ctx.createMediaStreamDestination());
|
|
393
|
-
const track = dst.stream.getAudioTracks()[0];
|
|
394
|
-
track.enabled = false;
|
|
395
|
-
return track;
|
|
396
|
-
})();
|
|
397
|
-
this._blankStream = new MediaStream([this._disabledSilence]);
|
|
398
405
|
this._localTracks = new LocalTracks();
|
|
399
|
-
this._localTracks.setTrack(this._disabledSilence.kind, this._disabledSilence);
|
|
400
406
|
this._localTracks.addEventListener('trackchanged', ({oldTrack, newTrack}) => {
|
|
401
407
|
// Normally the self-view UI only needs to be updated if the user clicks on something, but it
|
|
402
408
|
// also needs to be updated if the browser decides to end the local stream for whatever
|
|
@@ -621,8 +627,7 @@ exports.rtc = new class {
|
|
|
621
627
|
try {
|
|
622
628
|
const devices = [];
|
|
623
629
|
const addAudioTrack = updateAudio && this._selfViewButtons.audio.enabled &&
|
|
624
|
-
!this._localTracks.stream.getAudioTracks().some(
|
|
625
|
-
(t) => t !== this._disabledSilence && t.readyState === 'live');
|
|
630
|
+
!this._localTracks.stream.getAudioTracks().some((t) => t.readyState === 'live');
|
|
626
631
|
if (addAudioTrack) devices.push('mic');
|
|
627
632
|
const addVideoTrack = updateVideo && this._selfViewButtons.video.enabled &&
|
|
628
633
|
(!this._localTracks.stream.getVideoTracks().some((t) => t.readyState === 'live') ||
|
|
@@ -675,7 +680,7 @@ exports.rtc = new class {
|
|
|
675
680
|
for (const track of this._localTracks.stream.getAudioTracks()) {
|
|
676
681
|
// Re-check the state of the button because the user might have clicked it while
|
|
677
682
|
// getUserMedia() was running.
|
|
678
|
-
track.enabled =
|
|
683
|
+
track.enabled = this._selfViewButtons.audio.enabled;
|
|
679
684
|
}
|
|
680
685
|
const hasAudio = this._localTracks.stream.getAudioTracks().some(
|
|
681
686
|
(t) => t.enabled && t.readyState === 'live');
|
|
@@ -881,8 +886,9 @@ exports.rtc = new class {
|
|
|
881
886
|
minWidth: iw > width ? `${iw}px` : '',
|
|
882
887
|
minHeight: nh + ih > height ? `${nh + ih}px` : '',
|
|
883
888
|
});
|
|
884
|
-
})
|
|
885
|
-
|
|
889
|
+
});
|
|
890
|
+
if (isLocal) $videoContainer.prependTo($('#rtcbox'));
|
|
891
|
+
else $videoContainer.appendTo($('#rtcbox'));
|
|
886
892
|
this.updatePeerNameAndColor(this.getUserFromId(userId));
|
|
887
893
|
|
|
888
894
|
// For tests it is important to know when an asynchronous event handler has finishing handling
|
|
@@ -1279,7 +1285,7 @@ exports.rtc = new class {
|
|
|
1279
1285
|
logDisconnectErrorTimeout =
|
|
1280
1286
|
setTimeout(() => logErrorToServer(new Error('RTC connection lost'), 0), 10000);
|
|
1281
1287
|
($videoContainer.data('updateMinSize') || (() => {}))();
|
|
1282
|
-
await this.setStream(userId,
|
|
1288
|
+
await this.setStream(userId, new MediaStream());
|
|
1283
1289
|
});
|
|
1284
1290
|
peer.addEventListener('closed', () => {
|
|
1285
1291
|
_debug('PeerState closed');
|