ep_webrtc 0.1.100 → 2.0.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/README.md +76 -4
- package/ep.json +1 -0
- package/index.js +113 -28
- package/package.json +8 -4
- package/static/js/index.js +2 -2
package/README.md
CHANGED
|
@@ -120,6 +120,8 @@ example:
|
|
|
120
120
|
}
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
+
#### Ephemeral credentials
|
|
124
|
+
|
|
123
125
|
To limit abuse, the [coturn](https://github.com/coturn/coturn) TURN server
|
|
124
126
|
supports [ephemeral (temporary) usernames and
|
|
125
127
|
passwords](https://github.com/coturn/coturn/blob/60e7a199fe748cb7080594a458d22c2f7bb15a8c/README.turnserver#L664-L729).
|
|
@@ -151,13 +153,83 @@ Example:
|
|
|
151
153
|
"lifetime": 3600
|
|
152
154
|
}
|
|
153
155
|
]
|
|
154
|
-
}
|
|
156
|
+
},
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
There is also support for ephemeral credentials from the
|
|
160
|
+
[Xirsys](https://xirsys.com/) [API](https://docs.xirsys.com/?pg=api-turn):
|
|
161
|
+
|
|
162
|
+
* `credentialType` (required): Must be set to the exact string `"xirsys
|
|
163
|
+
ephemeral credentials"`.
|
|
164
|
+
* `url` (required): The desired Xirsys TURN API endpoint.
|
|
165
|
+
* `username` (required): Your Xirsys username.
|
|
166
|
+
* `credential` (required): Your Xirsys API secret.
|
|
167
|
+
* `lifetime` (optional; defaults to 43200 = 12 hours): How long (in seconds)
|
|
168
|
+
the ephemeral credentials will remain valid after the user visits a pad.
|
|
169
|
+
After this amount of time, new TURN connections will fail until the user
|
|
170
|
+
reloads the page (which will generate a new password).
|
|
171
|
+
|
|
172
|
+
Example:
|
|
173
|
+
|
|
174
|
+
```json
|
|
175
|
+
"ep_webrtc": {
|
|
176
|
+
"iceServers": [
|
|
177
|
+
{
|
|
178
|
+
"credentialType": "xirsys ephemeral credentials",
|
|
179
|
+
"url": "https://global.xirsys.net/_turn/myChannel",
|
|
180
|
+
"username": "myUsername",
|
|
181
|
+
"credential": "myPassword",
|
|
182
|
+
"lifetime": 3600
|
|
183
|
+
}
|
|
184
|
+
]
|
|
185
|
+
},
|
|
155
186
|
```
|
|
156
187
|
|
|
188
|
+
### Microphone Settings
|
|
189
|
+
|
|
190
|
+
The microphone can be configured by setting `audio.constraints` to any [audio
|
|
191
|
+
constraints](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#parameters)
|
|
192
|
+
value acceptable to client browsers. It has the following default value:
|
|
193
|
+
|
|
194
|
+
```json
|
|
195
|
+
"ep_webrtc": {
|
|
196
|
+
"audio": {
|
|
197
|
+
"constraints": {
|
|
198
|
+
"autoGainControl": {"ideal": true},
|
|
199
|
+
"echoCancellation": {"ideal": true},
|
|
200
|
+
"noiseSuppression": {"ideal": true}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
For a full list of available constraints, see [the
|
|
207
|
+
standard](https://www.w3.org/TR/2022/CRD-mediacapture-streams-20220307/#constrainable-properties).
|
|
208
|
+
|
|
157
209
|
### Video Sizes
|
|
158
210
|
|
|
159
|
-
|
|
160
|
-
|
|
211
|
+
The camera's record resolution can be configured by setting `video.constraints`
|
|
212
|
+
to any [video
|
|
213
|
+
constraints](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#parameters)
|
|
214
|
+
value acceptable to client browsers. It has the following default value:
|
|
215
|
+
|
|
216
|
+
```json
|
|
217
|
+
"ep_webrtc": {
|
|
218
|
+
"video": {
|
|
219
|
+
"constraints": {
|
|
220
|
+
"width": {"ideal": 160},
|
|
221
|
+
"height": {"ideal": 120}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
For a full list of available constraints, see [the
|
|
228
|
+
standard](https://www.w3.org/TR/2022/CRD-mediacapture-streams-20220307/#constrainable-properties).
|
|
229
|
+
|
|
230
|
+
Changing the record resolution does not change the size of the displayed video
|
|
231
|
+
widgets. To change the video widget size, set `video.sizes.small` and/or
|
|
232
|
+
`video.sizes.large`:
|
|
161
233
|
|
|
162
234
|
```json
|
|
163
235
|
"ep_webrtc": {
|
|
@@ -167,7 +239,7 @@ one or both of the following in your `settings.json`:
|
|
|
167
239
|
"large": 400
|
|
168
240
|
}
|
|
169
241
|
}
|
|
170
|
-
}
|
|
242
|
+
},
|
|
171
243
|
```
|
|
172
244
|
|
|
173
245
|
## Metrics
|
package/ep.json
CHANGED
package/index.js
CHANGED
|
@@ -14,21 +14,35 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
+
const _ = require('lodash');
|
|
18
|
+
const {Buffer} = require('buffer');
|
|
17
19
|
const crypto = require('crypto');
|
|
18
|
-
const log4js = require('ep_etherpad-lite/node_modules/log4js');
|
|
19
|
-
const statsLogger = log4js.getLogger('stats');
|
|
20
|
-
const configLogger = log4js.getLogger('configuration');
|
|
21
20
|
const eejs = require('ep_etherpad-lite/node/eejs/');
|
|
22
21
|
const sessioninfos = require('ep_etherpad-lite/node/handler/PadMessageHandler').sessioninfos;
|
|
23
22
|
const stats = require('ep_etherpad-lite/node/stats');
|
|
23
|
+
const util = require('util');
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
let logger = {};
|
|
26
|
+
for (const level of ['debug', 'info', 'warn', 'error']) {
|
|
27
|
+
logger[level] = console[level].bind(console, 'ep_webrtc:');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const defaultSettings = {
|
|
26
31
|
// The defaults here are overridden by the values in the `ep_webrtc` object from `settings.json`.
|
|
27
32
|
enabled: true,
|
|
28
33
|
audio: {
|
|
34
|
+
constraints: {
|
|
35
|
+
autoGainControl: {ideal: true},
|
|
36
|
+
echoCancellation: {ideal: true},
|
|
37
|
+
noiseSuppression: {ideal: true},
|
|
38
|
+
},
|
|
29
39
|
disabled: 'none',
|
|
30
40
|
},
|
|
31
41
|
video: {
|
|
42
|
+
constraints: {
|
|
43
|
+
width: {ideal: 160},
|
|
44
|
+
height: {ideal: 120},
|
|
45
|
+
},
|
|
32
46
|
disabled: 'none',
|
|
33
47
|
sizes: {large: 260, small: 160},
|
|
34
48
|
},
|
|
@@ -36,8 +50,18 @@ const settings = {
|
|
|
36
50
|
listenClass: null,
|
|
37
51
|
moreInfoUrl: {},
|
|
38
52
|
};
|
|
53
|
+
let settings = null;
|
|
39
54
|
let socketio;
|
|
40
55
|
|
|
56
|
+
const addContextToError = (err, pfx) => {
|
|
57
|
+
const newErr = new Error(`${pfx}${err.message}`, {cause: err});
|
|
58
|
+
if (Error.captureStackTrace) Error.captureStackTrace(newErr, addContextToError);
|
|
59
|
+
// Check for https://github.com/tc39/proposal-error-cause support, available in Node.js >= v16.10.
|
|
60
|
+
if (newErr.cause === err) return newErr;
|
|
61
|
+
err.message = `${pfx}${err.message}`;
|
|
62
|
+
return err;
|
|
63
|
+
};
|
|
64
|
+
|
|
41
65
|
// Copied from:
|
|
42
66
|
// https://github.com/ether/etherpad-lite/blob/f95b09e0b6752a0d226d58d8b246831164dc9533/src/node/handler/PadMessageHandler.js#L1411-L1420
|
|
43
67
|
const _getRoomSockets = (padId) => {
|
|
@@ -98,21 +122,70 @@ const handleErrorStatMessage = (statName) => {
|
|
|
98
122
|
if (statErrorNames.includes(statName)) {
|
|
99
123
|
stats.meter(`ep_webrtc_err_${statName}`).mark();
|
|
100
124
|
} else {
|
|
101
|
-
|
|
125
|
+
logger.warn(`Invalid ep_webrtc error stat: ${statName}`);
|
|
102
126
|
}
|
|
103
127
|
};
|
|
104
128
|
|
|
129
|
+
const fetchJson = async (url, opts = {}) => {
|
|
130
|
+
const c = new globalThis.AbortController();
|
|
131
|
+
const t = setTimeout(() => c.abort(), 5000);
|
|
132
|
+
let res;
|
|
133
|
+
try {
|
|
134
|
+
res = await globalThis.fetch(url, {signal: c.signal, ...opts});
|
|
135
|
+
} finally {
|
|
136
|
+
clearTimeout(t);
|
|
137
|
+
}
|
|
138
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
139
|
+
return await res.json();
|
|
140
|
+
};
|
|
141
|
+
|
|
105
142
|
exports.clientVars = async (hookName, {clientVars: {userId: authorId}}) => ({ep_webrtc: {
|
|
106
143
|
...settings,
|
|
107
|
-
iceServers: settings.iceServers.map((server) => {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
144
|
+
iceServers: await Promise.all(settings.iceServers.map(async (server) => {
|
|
145
|
+
switch (server.credentialType) {
|
|
146
|
+
case 'coturn ephemeral password': {
|
|
147
|
+
const {lifetime = 60 * 60 * 12 /* seconds */} = server;
|
|
148
|
+
const username = `${Math.floor(Date.now() / 1000) + lifetime}:${authorId}`;
|
|
149
|
+
const hmac = crypto.createHmac('sha1', server.credential);
|
|
150
|
+
hmac.update(username);
|
|
151
|
+
const credential = hmac.digest('base64');
|
|
152
|
+
return {urls: server.urls, username, credential};
|
|
153
|
+
}
|
|
154
|
+
case 'xirsys ephemeral credentials': {
|
|
155
|
+
const {
|
|
156
|
+
url,
|
|
157
|
+
username,
|
|
158
|
+
credential,
|
|
159
|
+
lifetime: expire = 12 * 60 * 60, // seconds
|
|
160
|
+
method = 'PUT',
|
|
161
|
+
headers: h = {},
|
|
162
|
+
jsonBody: b = {},
|
|
163
|
+
} = server;
|
|
164
|
+
// Can't set default values for the Content-Type and Authorization headers by using an
|
|
165
|
+
// object literal with spread (e.g., `{'content-type': 'foo', ...h}`) because the Headers
|
|
166
|
+
// constructor uses `.append()` internally instead of `.set()`. This matters if a header is
|
|
167
|
+
// repeated multiple times by using different mixes of upper- and lower-case letters.
|
|
168
|
+
const headers = new globalThis.Headers(h);
|
|
169
|
+
if (!headers.has('content-type')) headers.set('content-type', 'application/json');
|
|
170
|
+
if (username && !headers.has('authorization')) {
|
|
171
|
+
headers.set('authorization',
|
|
172
|
+
`Basic ${Buffer.from(`${username}:${credential}`).toString('base64')}`);
|
|
173
|
+
}
|
|
174
|
+
const body =
|
|
175
|
+
JSON.stringify(b && typeof b === 'object' ? {format: 'urls', expire, ...b} : b);
|
|
176
|
+
try {
|
|
177
|
+
const {v, s} = await fetchJson(url, {method, headers, body});
|
|
178
|
+
if (s !== 'ok') throw new Error(`API error: ${v}`);
|
|
179
|
+
return v.iceServers;
|
|
180
|
+
} catch (err) {
|
|
181
|
+
const newErr = addContextToError(err, 'failed to get TURN credentials: ');
|
|
182
|
+
logger.error(newErr.stack || newErr.toString());
|
|
183
|
+
throw newErr;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
default: return server;
|
|
187
|
+
}
|
|
188
|
+
})),
|
|
116
189
|
}});
|
|
117
190
|
|
|
118
191
|
exports.handleMessage = async (hookName, {message, socket}) => {
|
|
@@ -126,6 +199,22 @@ exports.handleMessage = async (hookName, {message, socket}) => {
|
|
|
126
199
|
}
|
|
127
200
|
};
|
|
128
201
|
|
|
202
|
+
exports.init_ep_webrtc = async (hookName, {logger: l}) => {
|
|
203
|
+
if (l != null) logger = l;
|
|
204
|
+
// TODO: Remove this once all supported Node.js versions have the fetch API (added in Node.js
|
|
205
|
+
// v17.5.0 behind the --experimental-fetch flag).
|
|
206
|
+
if (!globalThis.fetch) {
|
|
207
|
+
// eslint-disable-next-line node/no-unsupported-features/es-syntax -- https://github.com/mysticatea/eslint-plugin-node/issues/250
|
|
208
|
+
const {default: fetch, Headers, Request, Response} = await import('node-fetch');
|
|
209
|
+
Object.assign(globalThis, {fetch, Headers, Request, Response});
|
|
210
|
+
}
|
|
211
|
+
// TODO: Remove this once all supported Node.js versions have AbortController (>= v15.4.0).
|
|
212
|
+
if (!globalThis.AbortController) {
|
|
213
|
+
// eslint-disable-next-line node/no-unsupported-features/es-syntax -- https://github.com/mysticatea/eslint-plugin-node/issues/250
|
|
214
|
+
globalThis.AbortController = (await import('abort-controller')).default;
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
129
218
|
exports.setSocketIO = (hookName, {io}) => { socketio = io; };
|
|
130
219
|
|
|
131
220
|
exports.eejsBlock_mySettings = (hookName, context) => {
|
|
@@ -139,27 +228,23 @@ exports.eejsBlock_styles = (hookName, context) => {
|
|
|
139
228
|
context.content += eejs.require('./templates/styles.html', {}, module);
|
|
140
229
|
};
|
|
141
230
|
|
|
142
|
-
exports.loadSettings = async (hookName, {settings: {ep_webrtc = {}}}) => {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
// The own-property check on the target prevents prototype pollution. (Prototype pollution
|
|
148
|
-
// shouldn't be exploitable here because the source comes from admin-controlled settings.json,
|
|
149
|
-
// but the check is added anyway to hopefully pacify static analysis tools.)
|
|
150
|
-
if (Object.prototype.hasOwnProperty.call(target, k) && isObj(tv) && isObj(sv)) merge(tv, sv);
|
|
151
|
-
else target[k] = sv;
|
|
152
|
-
}
|
|
153
|
-
};
|
|
154
|
-
merge(settings, ep_webrtc);
|
|
231
|
+
exports.loadSettings = async (hookName, {settings: {ep_webrtc: s = {}}}) => {
|
|
232
|
+
settings = _.mergeWith({}, defaultSettings, s, (objV, srcV, key, obj, src) => {
|
|
233
|
+
if (Array.isArray(srcV)) return _.cloneDeep(srcV); // Don't merge arrays, replace them.
|
|
234
|
+
if (src === s.video && key === 'constraints') return _.cloneDeep(srcV);
|
|
235
|
+
});
|
|
155
236
|
settings.configError = (() => {
|
|
156
237
|
for (const k of ['audio', 'video']) {
|
|
157
238
|
const {[k]: {disabled} = {}} = settings;
|
|
158
239
|
if (disabled != null && !['none', 'hard', 'soft'].includes(disabled)) {
|
|
159
|
-
|
|
240
|
+
logger.error(`Invalid value in settings.json for ep_webrtc.${k}.disabled`);
|
|
160
241
|
return true;
|
|
161
242
|
}
|
|
162
243
|
}
|
|
163
244
|
return false;
|
|
164
245
|
})();
|
|
246
|
+
logger.info('configured:', util.inspect({
|
|
247
|
+
...settings,
|
|
248
|
+
iceServers: settings.iceServers.map((s) => s.credential ? {...s, credential: '*****'} : s),
|
|
249
|
+
}, {depth: Infinity}));
|
|
165
250
|
};
|
package/package.json
CHANGED
|
@@ -5,22 +5,26 @@
|
|
|
5
5
|
"url": "git@github.com:ether/ep_webrtc.git",
|
|
6
6
|
"type": "git"
|
|
7
7
|
},
|
|
8
|
-
"version": "0.
|
|
8
|
+
"version": "2.0.0",
|
|
9
9
|
"description": "WebRTC based audio/video chat to Etherpad",
|
|
10
10
|
"author": "John McLear <john@mclear.co.uk>",
|
|
11
11
|
"contributors": [],
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=12.17.0"
|
|
14
14
|
},
|
|
15
|
-
"dependencies": {
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"abort-controller": "^3.0.0",
|
|
17
|
+
"lodash": "^4.17.21",
|
|
18
|
+
"node-fetch": "^3.2.1"
|
|
19
|
+
},
|
|
16
20
|
"funding": {
|
|
17
21
|
"type": "individual",
|
|
18
22
|
"url": "https://etherpad.org/"
|
|
19
23
|
},
|
|
20
24
|
"devDependencies": {
|
|
21
25
|
"eslint": "^8.10.0",
|
|
22
|
-
"eslint-config-etherpad": "^3.0.
|
|
23
|
-
"typescript": "^4.
|
|
26
|
+
"eslint-config-etherpad": "^3.0.5",
|
|
27
|
+
"typescript": "^4.6.2"
|
|
24
28
|
},
|
|
25
29
|
"scripts": {
|
|
26
30
|
"lint": "eslint .",
|
package/static/js/index.js
CHANGED
|
@@ -668,8 +668,8 @@ exports.rtc = new class {
|
|
|
668
668
|
if (!addAudioTrack && !addVideoTrack) return new MediaStream();
|
|
669
669
|
debug(`requesting permission to access ${devices.join(' and ')}`);
|
|
670
670
|
const stream = await window.navigator.mediaDevices.getUserMedia({
|
|
671
|
-
audio: addAudioTrack,
|
|
672
|
-
video: addVideoTrack &&
|
|
671
|
+
audio: addAudioTrack && this._settings.audio.constraints,
|
|
672
|
+
video: addVideoTrack && this._settings.video.constraints,
|
|
673
673
|
});
|
|
674
674
|
debug('successfully accessed device(s)');
|
|
675
675
|
return stream;
|